1. State란 무엇인가?

우선 State를 설명하기전, Props(Properties)를 간단하게 살펴 보겠습니다. 일반적으로 React에서는 모든 데이터 전달 방향은 부모 컴포넌트에서 자식컴포넌트로 향해 있습니다. 그래서 자식컴포넌트에서 필요한 데이터는 부모컴포넌트에서 자식컴포넌트의 props에 담아 전달하게 됩니다.  이로써 자식컴포넌트는 부모컴포넌트로 부터 Props를 전달받아 사용이 가능합니다. 하지만 데이터를 수정하는 로직이나 사용자가 입력한 변경된 데이터를 저장해야 하는 경우, 읽기전용인 Properties(props)에서  변경 데이터 저장은 불가능합니다. 이에 모든 컴포넌트는 state가지고 있으며, state에서는 변경된 데이터를 관리할수 있습니다. 사용자와 컴포넌트 또는 컴포넌트와 컴포넌트 사이에 상호작용을하며 값을 바꿀수 있고, 데이터가 변경된 컴포넌트와 그 아래 자식컴포넌트는 리랜더링(Re-rendering)을 하여 사용자에게 바뀐 상태를 제공합니다.

아래는 자판기의 Drink 컴포넌트 내 생성자를 이용한 state 초기화 방법입니다.

class Drink extends React.Component {
  //
    constructor(props: any) {
        super(props);
        this.state = {
            name: 'Coke',
            price: 1000,
            quantity: 1
        }
    }
...

위 Drink 컴포넌트는 props를 받아 React.Component의 생성자 파라미터로 사용되고, Drink의 State는 (name, price, quantity) 3가지 필드로 구성되어있으며, 각 필드는 기본값(Default)값으로 초기화 되어있습니다. 따라서 이 컴포넌트 내에서 자신의 state의 필드값을 자유롭게 사용할수 있으며, 원한다면 리랜더링(Re-rendering)하여 변경된 데이터를 표현할 수 있습니다.

아래는 생성자(constructor)를 사용하지않고 state를 선언하는 방법입니다.

class Drink extends React.Component {
  //
    state = {
        name: 'Coke',
        price: 1000,
        quantity: 1
    }
  ...

‌모든 컴포넌트는 자신의 state를 가지기 때문에, 생성자(constructor)에 초기화 하지않고 위 처럼 클래스내 필드로 선언하여 사용할 수도 있습니다.

2. State와 컴포넌트(Component)

React에서 관리하는 모든 데이터는 부모컴포넌트에서 자식컴포넌트로 이동합니다. 물론 callback 함수를 통해 부모로 데이터를 리턴하여 전달할 수 있지만, 기본적으로 부모에서 자식으로 향합니다. 그렇다면 부모 컴포넌트와 자식 컴포넌트가 각자 자신의 state를 가지고 있다면 부모컴포넌트의 state는 자식컴포넌트의 state에 영향을 끼칠수 있을까요? 그렇지 않습니다. 자식과 부모 또는 또다른 자식컴포넌트가 있다고 해도 모두 상관없이 각 컴포넌트는 인스턴스화 될때 각기 독립적으로 관리가 됩니다. 이는 아래 자판기(VendingMachine)컴포넌트로 확인해보겠습니다. VendingMachine 컴포넌트의 render 메소드의 일부분 입니다.

class VendingMachine extends React.Component {    
  ...
		render() {
        return (
            <SegmentGroup size='small'>
                <Segment inverted color='blue' floated='left'>
                    <SegmentGroup vertical>
                        <SegmentGroup horizontal>
                            <Drink/>
                            <Drink/>
                            <Drink/>
                            <Drink/>
                        </SegmentGroup>

위는 Drink 컴포넌트 4개가 선언 되어있고, 각 Drink 컴포넌트 인스턴스는 State는 기본값으로 초기화 됩니다. 그 이후 Drink 컴포넌트 인스턴스내 변경사항이 생기면,  인스턴스는 자신의 State를 각기 관리하게 됩니다. 위 처럼 독립적으로 관리되는 컴포넌트의 변경사항을 반영하기 위해서, 각 컴포넌트내 setState() 메소드를 사용하여 리랜더링을 합니다.

3. 리랜더링(Re-rendering)

각 컴포넌트는 자신의 state를 가지고있고 , state의 변경사항을 반영하기 위해선 컴포넌트 내 setState() 메소드를 통해 리랜더링을 할수 있습니다.  우선 setState() 사용법을 알아보겠습니다.

class Drink extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            name: 'Coke',
            price: 1000,
            quantity: 10
        }
    }
  
    buySomeDrink = () => {
        this.setState({
            quantity: this.state.quantity - 1
        });
    }

    render() {

        const { name, price, quantity } = this.state;

        return (
            <Segment>
                <Image src={sodaImage} size='mini' centered />
                <Card.Content>
                    <Card.Header textAlign='center'>{name}</Card.Header>
                    <Card.Meta textAlign='center'>₩ {price}</Card.Meta>
                </Card.Content>
                <Card.Content extra>
                    <Card.Header textAlign='center'>
                        <Icon name='cubes'/><Label content={quantity}/>
                      	<Button onClick={this.buySomeDrink}>Buy</Button>
                    </Card.Header>
                </Card.Content>
            </Segment>
        );
    }
}

위 코드는 버튼의 onClick 이벤트 발생시 컴포넌트 내 buySomeDrink() 메소드를 호출합니다.  buySomeDrink() 함수가 호출될때 현재 컴포넌트의 state의 필드중 quantity값을  (this.state.quantity -1) 로 변경하고 setState()를 통해 리랜더링을 합니다. 이로써 버튼의 이벤트 발생시 Drink 컴포넌트의 변경사항을 반영하게 됩니다.

리랜더링을 할땐 변경된 데이터 적용 후 해야하지만,  state 필드값이 한 메소드 수행 후변경된 데이터로 리랜더링 한다는 보장은 없습니다. 물론 우리의 눈으로 보기에 코드의 순서대로 메소드호출 후 state를 변경하여 리랜더링이 되는것 처럼보이지만, setStat() 메소드는 비동기(Asynchronous)  방식으로 호출하기 때문에 메소드 호출 후 값을 변경시키는 작업을 마칠때 까지 기다려 주지 않습니다. 그래서 컴포넌트 내 오래걸리는 메소드가 있다면, 메소드가 state 필드값을 변경하기전 기존 값으로 리랜더링을 하게 됩니다. 그래서 메소드로 state를 변경후 setState() 를 하고 바로 아래 console.log(this.state)를 호출하면 변경된 state 값이 아닌 변경전 값을 로그에 출력하기도 하므로, 언젠가 예기치 못한  문제가 발생할 수 있습니다. 따라서 메소드 호출 후 state의 변경사항을 반영 후 리랜더링하기 위해서, 아래와 같이 작성해야 합니다.

this.setState({quantity: this.state.quantity - 1}, function() {
	// 함수를 통해 처리할 로직 작성.
});

위와 같이 setState() 의 매개변수를 ({변경될 state 필드: 값}, 함수) 형태로 작성하면, 현재 Drink 컴포넌트의 state 필드인 quantity 값을 먼저 업데이트 한 후 함수가 호출되기 때문에 변경된 state 필드값으로 리랜더링 할 수 있습니다.

다음은 여러 setState() 함수 호출시 순서대로 호출되는 방법을 알아보겠습니다. 우선  setState() 함수는 비동기로 호출이 됩니다. 따라서 무엇이 먼저 처리되어 변경사항을 반영을 하는지는 개발자 입장에선 알 수가 없습니다. 따라서 여러 setState() 메소드가 선언되어있다면 코드 순서대로 호출은 하겠지만, 순서대로 처리하여 리턴하지 않는것을 알 수 있습니다.

아래와 같이 작성하여  setState() 함수를 순서대로 호출해 보겠습니다.

this.setState((state, props) => {
  return {
    quantity: this.state.quantity - 1
  }
});
this.setState((state, props) => {
  return {
    price: this.state.price + 300
  }
});
this.setState((state, props) => {
  return {
    name: 'Fanta'
  }
});

위 처럼 코드를 작성하면 작성된 순서대로 setState() 메소드가 처리됩니다. setState의 매개변수(Parameters)를 익명함수로 하고, 익명함수의 매개변수를 자신 이 포함된 컴포넌트의 state와 props를 주었습니다. 이렇게 되면 setState 내 익명함수가 반환하기 전까지 다음 setState() 메소드를 호출을 하지 않게 됩니다.  그 이유는 각 setState() 메소드는 자바스크립트 내부에서 실행할 메소드가 queues에 순서대로 쌓여있고, 각 setState() 메소드는 매개변수로 들어가있는 함수의 값이 반환되기 전까지 진행을 막기(Blocking)하기 때문입니다. 이처럼 매개변수를 함수형으로 작성하게 되면 state의 변경 순서를 제어할 수 있고,  함수형은 매개변수에 따라 같은 수행 결과를 반환하기 때문에 예외상황을 방지할 수도 있습니다. 따라서 setState의 함수형 스타일로 작성을 하는것은 좋은 코딩 방법중 하나입니다.

4. setState() 메소드와 병합(Merge)방식

setState() 함수에서 발생하는 병합은 얕은병합(Shallow Merge), 깊은 병합(Deep Merge) 두가지로 나뉩니다. 얕은 병합은 변경사항을 기존의 state에 덮어씌우는 방식으로 병합을 합니다. 깊은 병합은 state의 모든 필드(전체)를 변경하지않고, 변경이 일어난 필드에 대해서만 병합을 합니다. 두 가지 병합은 예시는 아래와 같습니다.

   ...
   constructor(props) {
        super(props);
        this.state = {
            name: {
              product: 'Coke',
              manufacturer: 'Coca_cola'
            },
            price: 1000,
            quantity: 10
        }
    }
  	//얕은 병합
    modifyProduct = () => {
        this.setState({
            name: {product:'Fanta'}
        });
    }
    /*  state는 아래와 같이 병합됩니다.        
    		name: {
              product: 'Fanta',
            },
            price: 1000,
            quantity: 10
        }
    */
    
...
    
    //깊은 병합
    modifyProduct = () => {
        this.setState({
            name: {
              product:'Fanta',
          	  manufacturer: 'Coca_cola'
            }
        });
    }
    /*  state는 아래와 같이 병합됩니다.        
    		name: {
              product: 'Fanta',
              manufacturer: 'Coca_cola'
            },
            price: 1000,
            quantity: 10
        }
    */
    ...

setState() 함수는 기본적으로 얕은병합(Shallow Merge)을 합니다. 얕은병합은 State의 내부 객체를 변경하면 얕은병합이 발생하며, 기존객체를 setState 를 통해 받은 객체로 변경시킵니다. 병합시에는 병합하는 객체만 병합합니다. 그외는 무시되어 덮어 씌워집니다. 이에 반해 깊은병합(Deep Merge)는 기존의 상태를 지우지 않으며, 전체를 변경하지 않고 변경된 부분만 교체됩니다.

5. 어떤 데이터를 State에서 관리하면 좋을까?

우리는 컴포넌트내 데이터를 props나 state로 관리할 수 있습니다. state는 언제든지 업데이트 될 수 있으며, 컴포넌트는 이에 리랜더링을 하기 때문에, 리랜더링시 참조하는 데이터는 State에 저장하는 것이 좋습니다. 만약 한 필드의 데이터가 변경사항이 있으나 리랜더링시 보여지는 변화가 없다면 그 필드는 state에 들어갈 필요가 없습니다.

아래는 state에서 관리하면 좋은 데이터의 종류입니다.

  • 사용자가 입력한 데이터(textbox, form field)
  • 현재 또는 선택된 아이템(현재 탭, 테이블내 선택된 행)
  • 서버로 부터 받는 데이터(객체 리스트)
  • 동적인 상태(모델을 열기 및 닫기, 사이드바 열기 및 닫기)
React + MobX
SPA 라이브러리인 React를 MobX state 관리 라이브러리와 함께 배워봅시다.

참조자료: Git

https://github.com/namoosori/react-blog