프로그래밍 공부하기

React LifeCycle 1 - 생성 단계 본문

Web/[JS] FrontEnd

React LifeCycle 1 - 생성 단계

ihl 2021. 12. 18. 22:59

React LifeCycle

  리액트의 모든 컴포넌트는 생성-업데이트-제거라는 생명주기를 가지며, 각 생명주기에 실행되는 생명주기 메서드들을 가진다. 즉, 리액트 컴포넌트가 생성되고 제거될 때까지의 과정 안에서 특정 시점에 이에 매칭되는 특정 메서드가 호출된다. 생명주기 메서드들은 개발자가 재정의하여 개발자가 원하는 시점에 원하는 로직이 실행되게 만들 수 있다. 이번 포스팅에선 생성 단계의 메서드에 대해 알아보자.

1. 생성 단계 (now)
2. 업데이트 단계
3. 제거 단계

 

1. constructor()

class Item extends React.Component {
  constructor(props){
    super(props);
    
    this.state = { // state 초기화
      name: 'ihl',
      gender: 'female'
    }
    
    this._addProperty = this._addProperty.bind(this) // 메서드 바인딩
  }
  
  _addProperty(obj) {
    this.setState((prevsState) => {
      return {...prevState, ...obj}
    })
  }
  
  render() { /* ... */ }
}

  리액트 컴포넌트가 생성될 때 가장 먼저 호출되는 것은 생성자 함수인 constructor() 이다. constructor()는 크게 2가지 목적으로 리액트 사용하는 개발자에 의해 구현된다.

 

  첫 번째는 지역 상태(state)의 초기화이다. 리액트 컴포넌트의 상태는 일반적으로 setState() 라는 함수를 통해 이루어지지만, constructor에서만은 상태를 직접  할당하여 상태를 초기화하는 역할을 한다. 반대로 이야기하자면, constructor() 내부에서는 setState()를 호출해서는 안된다는 의미이기도 하다.

 

  두 번째로는 컴포넌트 메서드의 바인딩이다. DOM 요소들의 이벤트로 등록되는 함수의 경우 this가 이벤트가 발생한 요소를 가리키게 되어, 이벤트 발생 시, 컴포넌트의 상태에 접근할 수 없게되는 등의 예상치 못한 결과가 발생할 수 있다. 따라서 컴포넌트의 이벤트 처리에 사용되는 메서드의 경우 constructor에서 this를 바인딩해주어야 한다.

 

constructor(props) {
 super(props); // 반드시 해야할 것!
 this.state = { color: props.color };  // 절대 하면 안되는 것!
}

  constructor()를 직접 구현 시, 주의할 점은 constructor() 구현의 가장 첫 줄에는 super(props)를 호출해야한다. 그렇지 않으면, this.props 가 생성자 내에서 정의되지 않아 버그를 발생시킬 수 있다. 반면, state에 props 요소를 복사하는 것은 해서는 안되는 작업이다. props의 변화에 state가 감지할 수 없게되기 때문이다.

 

let props = 1;
let state = props;

props = 2;
console.log(state); // 당연히 출력 결과는 '1'이다.

  예를 들어 위 코드와 같이 변수 props에 1을 할당하고, 변수 state에 a를 할당한 후, props를 바꿔보자. 이 경우 당연히 state는 변화하지 않는다. 이와 같은 일이 react에서도 똑같이 발생하는 것이다. 상위 컴포넌트의 props가 현재 컴포넌트에 영향을 주어야 한다면, 그대로 this.props를 사용하는 것이 좋다. 만약 props가 현재 컴포넌트의 초기 상태에만 영향을 준다면, props의 이름을 initialColor와 같은 이름으로 바꾸고 위와 같이 사용하면 된다. (state의 변화는 현재 컴포넌트 내의 이벤트 함수를 통해 setState를 호출하여 발생시키게 한다.)

 

 

2. getDerivedStateFromProps()

static getDerivedStateFromProps(nextProps, preState) {
  // preState: 이전에 props로 부터 파생된 값
  // nextProps: 현재 props로 받은 값
  
  // 리턴된 state 값은 이전 props와 현재 props 모두에게 영향을 받는다.
  // = state는 props의 변화에 영향을 받아야 한다.
  if(nextProps.color !== preState.color) return { color: nextProps.color }
  return null;
}

  getDerivedStateFromProps()는 컴포넌트 최초 생성 시, constructor() 호출 직후, props나 state가 변경되었을 때, forceUpdate() 호출되어 강제로 재렌더링이 되어야할 때 호출되는(React v16.4 이상 기준) 함수이다. getDerivedStateFromProps()는 state가 props에 의존하는 경우. 특히, 현재 컴포넌트의 상태 값이 props의 이전 값과 이후 값 모두에게 의존해야하는 경우에만 극히 드물게 사용된다.

 

  getDerivedStateFromProps()를 권장하지 않는 방식으로 사용하는 경우들이 종종있다.

 

  예를 들어 첫 번째로, props 변화에 따른 부수효과를 발생시켜야 한다는 경우, getDerivedStateFromProps()보단 componentDidUpdate()를 사용하는 것이 좋다. getDerivedStateFromProps()는 this로 인스턴스에 접근할 수 없다는 한계가 있기 때문이다.

 

  두 번째로, props 변화 시 일부 데이터를 기억하거나 다시 계산하는 memoization이 필요한 경우엔 getDerivedStateFromProps()에선 props와 state의 변경 사항을 별도로 추적하고 감지해야하기 때문에 적절하지 않다. props 변화에 따라 render여부를 결정하는 PureComponent와 sCU(shouldComponentUpdate())나, memoization 도구들을 사용하는 것이 낫다.

 

// 1. 완전 제어 컴포넌트 :
//     - props에 영향받는 state를 만들지 않고, props를 바로 사용한다.
function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email}></input>
}

// 2. 완전 비제어 컴포넌트 : 
//     - state를 초기화할 때만 props를 복사해서 사용하여, 이후 props 변화에 영향을 안받게 한다.
class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}

  마지막으로, props가 변화할 때 일부 state를 재설정하고 싶은 경우이다. 이 경우는 2가지 케이스로 나뉜다. 첫 번째는 state가 계속 props에 영향을 받는 케이스다. 이 때는 props 변화에 따른 값이 필요한 부분을 별도의 state로 지정하지 않고, 바로 props로 표현(완전제어 컴포넌트)하는 것이 좋다. 두 번째는 최초 state 생성 시에만 초기 값으로 props를 사용하는 경우이다. 이 때는 getDerivedStateFromProps()를 사용하지 않고, 상태의 초기 값에 바로 props의 복사 값을 넣어주면 된다.

 

  getDerivedStateFromProps()는 반드시 static을 필요로하며, 이 때문에 내부에서는 this로 클래스의 인스턴스를 조회할 수 없다. 또한, getDerivedStateFromProps()의 리턴 값은 컴포넌트의 상태(state)로 설정되며, null을 반환하면 아무 일도 발생하지 않는다.

 

3. render()

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

  render()는 컴포넌트가 화면에 그릴 요소들을 결정하는 메서드로, 컴포넌트 정의 시 반드시 작성해야한다. render()가 반환 가능한 리턴값은 5가지 유형이 있다.  jsx를 사용하여 생성되는 하나의 React Element(DOM 노드 혹은 사용자 정의 컴포넌트), Fragment로 둘러싼 React Element들, Portal, 문자열과 숫자(DOM에서 text 노드로 표현됨), Boolean과 null(아무 것도 렌더링 하지 않음)이 그것이다. 

 

  render()는 반드시 순수함수로 작성되어야 한다. 즉, 컴포넌트의 state에 영향을 주는 등의 사이드 이펙트가 발생하지 않고, 호출될 때마다 동일한 결과를 반환해야하며, 브라우저와 직접적으로 상호작용을 하지 않아야 한다. 예를 들어 render() 함수에서 setState()를 호출한다면, render() 내에서 호출된 setState()에 의해 state가 변경되고, 이에 따라 render() 가 재호출되는 무한 루프에 빠질 수도 있다.

 

4. componentDidMount()

  componentDidMount()는 render()에서 리턴된 값이 마운팅(DOM 트리에 추가) 직후에 호출된다. 따라서 DOM 노드에 접근하는 코드 등 DOM 노드가 필요한 초기화 작업은 componentDidMount()에서 수행하는 것이 좋다. 예를 들어 DOM 노드의 크기나 위치를 측정해야하는 경우, 외부로의 데이터 요청, 데이터 구독 등이 수행된다.(단, componentDidMount()에서 데이터를 구독했다면, componentWillUnmount()에서는 구독을 해제하자)

 

  특히, componentDidMount()에서 호출된 setState()는 브라우저가 화면을 갱신하기 전에 추가적인 렌더링을 발생시킨다. 즉, 두 번의 render()가 호출되었지만, 사용자는 마지막 한 번의 render() 결과만을 볼 수 있다. 이러한 방식은 componentDidMount()에서 비동기 통신을 수행한 후 setState()를 호출해 컴포넌트의 상태를 바꿀 때 유용하지만, 성능 문제로 이어지기 쉽기 때문에 주의가 필요하다.

 


React component : https://ko.reactjs.org/docs/react-component.html

파생값을 사용해야할까? : https://ko.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#when-to-use-derived-state

React 생명주기 : https://velog.io/@kwonh/React-

Comments