본문 바로가기

Study/React

Class Component | state, ref, pureComponent

 

setState

 

setState( { dataKey in stateObj : any value, callBack? } )

class 문법을 이용해서 만든 component는 state를 가질 수 있다. 리액트는 컴포넌트의 state가 변경 되면, 자동으로 render 함수를 다시 호출한다. 

이 때 state에 정의되어있는 데이터를 직접 바꾸는 것이 아니라, setState라는 API를 이용해야 한다. encapsulation의 관점에서 데이터를 관리하는 것과, 리액트가 setState에 의해 변경되는 state의 변화만 알아차릴 수 있다는 사실 때문이다. 

 

class Habit extends Component {
  state = {
    count: 0,
  };

  handleDecrement = () => { // arrow function in obj
    const count = this.state.count - 1;
    this.setState({ count: count < 0 ? 0 : count }); // here! 
  };

  render() {
    return (
        <button
          className='habit-btn habit-decreament'
          onClick={this.handleDecrement}
        >
        </button>                 
    );
  }
}

 

here! this.setState( { count : count < 0 ? 0 : count } )에서 사용된 두 색상의 count는 이름은 같지만 다른 값을 가지고 있다.

먼저 왼쪽에 사용된 분홍색 count는 "state"안에서 관리되고 있는 data count를 가리키는 것이고, 오른쪽의 녹색 count는 method(handleDecreament) 내부에서 정의한 this.state.count - 1을 가리키는 것이다. 

refs and DOM | reactjs.org

 

vanila js에서 DOM 요소를 조작할 때에는 document.querySelector()를 이용했다. 그러나 리액트는 컴포넌트의 state가 변경되면 render를 자동으로 호출하고, UI에서 다르게 보여질 부분을 virtual DOM tree에 표기한다. 즉 직접적으로 DOM요소를 조작하지 않는다. 따라서 리액트에서 DOM 요소를 조작하려면, 리액트가 제공하는 createRef라는 API를 이용해야 한다.

 

Refs provide a way to access DOM nodes or React elements created in the render method.

 

Return value of createRef

먼저 클래스 내부에 변수를 만들어서, createReact를 호출한 결과를 할당한다. 그런 다음, 조작하고 싶은 리액트 컴포넌트나, DOM 요소의 ref attribute에 선언한 변수를 할당한다. 

 

class Input extends Component {
  inputRef = React.createRef();
  render() {
    return (
      <div className='input-container'>
        <input
          type='text'
          defaultValue={'Habit'}
          className='input-habit'
          ref={this.inputRef} // attach! 
        />
        <button
          className='input-add'
          onClick={() => console.log(this.inputRef)}
        >
          Add
        </button>
      </div>
    );
  }
}

 

버튼을 클릭하면 input 태그에 연결된 inputRef에 어떤 데이터가 들어있는지, 콘솔에 출력되도록 코드를 작성했다.

 

React.createRef()

 

createRef의 호출 결과, 객체 데이터가 출력된다. 이 데이터의 current에는 우리가 inputRef가 연결되어 있는 속성의 주인 태그인 input 태그 요소가, 이 요소의 value에는 input에 현재 입력되어있는 'Habit'이라는 유용한 데이터가 각각 들어있다.  

 

Class & Function Component 

컴포넌트 class 컴포넌트 function 컴포넌트
만드는 방법 React.Component function
React.PureComponent memo
React Hook

 

지난 챕터에서 component와 props, state에 대해 간단하게 정리 해 보았다. 지금까지는 주로 class의 component를 이용해서 리액트 요소를 구현했다. PureComponent는 무엇일까? 

 

PureComponent 이야기를 하려면, render에 대해 다시 한번 상기해 보아야 한다. render는 어떤 class component의 state나 props가 변경되었을 때, 그 컴포넌트를 VDOM tree에 다시 렌더링하는 메서드이다. VDOM tree는 실제 DOM이 아니라, 가상의 DOM을 업데이트 하는 것이기 때문에 성능에 큰 문제를 일으키지 않는다. 

 

하지만 이런 상황을 생각해 보자. 예를 들어, 컴포넌트A의 자식 컴포넌트인 a는 업데이트 될 때 마다, 즉 render가 호출될 때마다 실행되는 함수(life cycle method)를 메서드로 가지고 있고, 이 메서드는 무거운 일을 수행한다. 컴포넌트a의 state나 props에 변화가 생기지 않더라도, A에서 변화가 생기면  컴포넌트A 뿐만 아니라 a의 render도 호출되기 때문에, 이 무거운 작업을 수행하는 함수가 매번 실행된다면 성능에 문제를 일으킬 수 있다. 이런 문제를 막기 위해 사용되는 것이 PureComponent이다. 

 

PureComponent & shallow comparison

PureComponent는 컴포넌트와 비슷하지만, 컴포넌트에서 구현하고 있지 않은 "shouldComponentUpdate"라는 메서드를 가지고 있다. 이 메서드는 컴포넌트의 state와 props를 이전의 값과 얕게 비교해서(shallow comparison) 실제로 데이터가 변하지 않았다면 render를 호출하지 않는다. 

최초에 UI로 렌더링 된 후에 업데이트 되지 않는다면, 즉 다시 render할 일이 없는 컴포넌트라면 PureComponent로 만드는 것이 적절하다. 

 

shallow comparison은 참조하고 있는 메모리의 주소가 같은 데이터는, 실제 그 내용이 다르더라도 같은 데이터라고 판단한다.

예를 들어 어떤 컴포넌트가 아래의 habit이라는 객체를 props으로 가지고 있다면, habit.count가 2나 3으로 변해도 habit이 참조하고 있는 메모리 주소는 변하지 않기 때문에, habit의 데이터가 바뀌었다고 판단하지 않는다. 즉 props에 변화가 없기 때문에, 이 컴포넌트가 pureComponent로 만들어졌다면 render는 호출되지 않는다. (그냥 component로 만들어졌다면, shallow고 뭐고 무조건 render를 호출한다.)

 

+) 그냥 Component에서는 state를 직접 수정해도, 우리가 원하는 대로 UI를 업데이트 할 수 있다(setState를 이용하면 무조건 다시 렌더링하기 때문에). 하지만 나중에 예상치 못한 버그를 만날 수 있기 때문에 직접 state를 수정하는 것은 항상 피한다. 리액트 뿐만 아니라 다른 모든 프로그래밍에서 통용되는 원칙이다. 

 

 

한가지 예를 더 들어보자. 아래의 코드에서 state에 있는 habits의 어떤 요소의 count를 1씩 증가시켜도, habits은 같은 배열 데이터이다. 게다가 PureComponent로 만들어졌기 때문에 count가 바뀌어도, state는 변하지 않았기 때문에 렌더링은 발생하지 않는다. 이것은 우리가 리액트에서 state를 직접 수정하지 않는 이유 중 하나이다. 

 

class App extends PureComponent {
    handleIncreament = (habit) => {
        const index = this.state.habits.indexOf(habit);
        this.state.habits[index].count++;
        this.setState({ habits: this.state.habits });
      };
}

 

shallow copy를 피하는 방법

오른쪽의 코드는 shallow copy를 피할 수 있다

 

왼쪽의 13번째 줄 코드를 살펴보면, state의 habits에 들어있는 아이템들을 spread operator를 이용해서 하나 하나씩, 새로운 배열(copiedHabits)에 할당했다. 이 경우에 copiedHabits은 state의 habits과는 다른 메모리 주소에 할당이 된다. 그렇기 때문에 만약 App이 PureComponent였다고 해도, setState에서 habits에 새로운 메모리 주소를 갖는 copiedHabits을 할당해 주었기 때문에, state에 변화가 생긴 것으로 판단하고 렌더링이 정상적으로 이루어진다. 하지만 habits의 아이템copiedHabits의 아이템이 객체 데이터라면, 여전히 같은 주소를  참조하는, 같은 객체일 것이다. 

 

오른쪽의 38번째 코드부터 쭉 살펴보면, state의 habits에 map 함수를 이용해서 새로운 배열을 반환한다. 그런데 그 배열의 각 아이템을 그대로 가져오는 것이 아니라, spread operator를 이용해서 하나 하나씩 새로운 객체에 할당한다. 이 때 count라는 key의 값을 1만큼 증가해서 새로운 객체 아이템들로 이루어진 배열을 반환한다. 이제는 habits의 아이템과 새로운 객체로 반환된 아이템은 서로 다른 주소를 참조하는, 다른 객체가 된다. 

 

언제 Pure를 쓰고, 언제 그냥 Component를 쓸까? 

state를 직접 관리하고 있고, state 데이터의 변화가 빈번하게 일어나는 컴포넌트에서는 매번 변화가 일어났는지 아닌지 검사하는 pure보다 그냥 컴포넌트를 쓰는 것이 좋을 수 있다. 

 

⚠ 주의 사항 

'Study > React' 카테고리의 다른 글

MVC & Dependency Injection  (0) 2022.04.29
PostCSS  (0) 2022.04.23
Deployment  (0) 2022.04.22
Function Component | memo, React Hooks  (0) 2022.04.22
Class Component | Life cycle method  (0) 2022.04.21