본문 바로가기

0/web

뒤늦은 React 입문기 정리 (라이프 사이클, 렌더링 과정)

반응형

render() 함수를 호출할 때 render() 함수는 React에서 사용하는 타입의 컴포넌트를 생성.

이 때 생성하는 컴포넌트는 다음과 같다.

  • ReactCompositeComponent 객체 : DOM이 아닌 컴포넌트를 생성할 때 사용됨.
  • ReactDOMComponent 객체 : DOM을 만들 때 사용하는 컴포넌트.

render() 함수가 생성한 컴포넌트를 React 컴포넌트에 마운트하기 위해 ReactReconciler.mountComponent()  메서드를 호출. 이 메서드에서는 ReactCompositeComponent 객체와 ReactDOMComponent 객체의 mountComponent() 메서드를 호출하며, 이 시점에 주요 작업이 시작됨.

ReactCompositeComponent 객체의 주요 작업은 다음과 같다.

  • constructor() 메서드 실행
  • componentWillMount() 메서드 실행
  • 렌더링 실행
  • 배치 처리 작업(ReactReconcile Transaction 객체)에 메서드나 속성 등록
    • componentDidMount() 메서드가 있으면 componentDidMount() 메서드 등록
    • ref 속성이 있으면 attachRefs 속성 등록
  • 하위 ReactComponent 객체가 있으면 ReactComponent 객체를 생성하고 다시 ReactReconciler.mountComponent() 메서드를 실행

이 후에 자식 컴포넌트로 내려가면서 ReactDOMComponent 객체와 ReactCompositeComponent 객체의 주요 작업을 반복적으로 실행하며 Virtual DOM을 생성한다.

이렇게 Virtual DOM이 완성된 후 등록된 배치 처리 작업이 다음과 같이 실행된다.

  • ReactReconcileTransaction 객체
    • componentDidMount() 메서드를 실행한다(componentDidXXX() 메서드 실행 시점에는 DOM에 접근할 수 있다).
    • 기본 이벤트를 등록한다.
    • 추가한 이벤트를 등록한다.
    • ref 속성 추가 등 기타 작업을 실행한다.
  • ReactDefaultBatchingStrategy 객체
    • componentWillMount() 메서드와 componentDidMount()메서드에서 상태를 변경했다면 이 시점에 상태를 갱신하는 작업이 시작된다.

 

Reconciliation 작업

Virtual DOM을 만든 후 Virtual DOM을 갱신할 때 Reconciliation 작업을 한다. Reconciliation 작업은 Virtual DOM과 DOM을 비교해 DOM을 갱신하는 작업이다. Virtual DOM을 갱신하는 방법은 2가지이고 갱신하는 유형도 2가지다.

Virtual DOM 갱신 방법

Virtual DOM을 갱신하는 방법은 크게 두가지로 나뉜다.

  1. setState() 메서드를 호출해 해당 컴포넌트를 변경 대상 컴포넌트(dirty component)로 등록해 갱신하는 방법
  2. Redux에서처럼 스토어가 변할 때 다시 최상위 컴포넌트의 render() 함수를 호출해 최상위 컴포넌트를 변경 대상 컴포넌트로 등록하는 방법

두 방법의 차이점은 비교를 시작하는 컴포넌트가 다르다는 점이다. setState() 메서드는 해당 컴포넌트를 기준으로 갱신하는 반면, render() 함수는 제일 위부터 Virtual DOM의 갱신이 필요한지 확인하기 시작한다. 그 이후에 비교하는 방법은 같다.

setState() 메서드를 호출하는 방법은 컴포넌트의 state가 변경됐을 때 Virtual DOM을 갱신하는 방법이다.

React의 state를 변경해서 컴포넌트를 갱신할 때는 직접 this.state 속성에 접근해서 state를 변경하는 방법과 setState() 메서드로 state를 변경하는 방법이 있다. 이 두가지 방법은 비슷해보이지만 다르게 작동한다.

state를 변경하면 바로 컴포넌트에 반영되지 않고 해당 컴포넌트가 변경 대상 컴포넌트로 등록되고 나중에 배치 처리 작업에서 변경 대상 컴포넌트를 갱신한다. 만약 this.state 속성으로 직접 state를 수정하면 변경 대상 컴포넌트로 등록되지 않기 때문에 등록되기 전까지 컴포넌트가 갱신되지 않는다. 그래서 바로 state 변경을 반영하고 싶다면(동기로 반영된다는 얘기가 아니라 배치 처리 작업에 등록된다는 의미) setState() 메서드를 사용해 변경해야 한다.

setState() 메서드를 실행하면 변경 대상 컴포넌트를 ReactUpdates.enqueueUpdate() 메서드에서 등록한다. 등록된 변경대상 컴포넌트는 ReactDefaultBatchingStrategy.close() 메서드에 등록한 ReactUpdates.flushBatchedUpdates() 메서드에서 갱신한다.

render() 함수를 호출하는 방법은 store가 변경됐을 때 Virtual DOM을 갱신하는 방법이다.

Redux를 사용하고 있다면 하나의 store를 가지고 있기 때문에 store가 변경됐을 때 다시 최상위 컴포넌트의 render() 함수를 호출해 Virtual DOM을 갱신하는 작업을 한다. 갱신하는 작업은 setState() 메서드를 사용할 때와 크게 다르지 않지만 최상위 컴포넌트부터 비교한다는 점이 다르다.

최상위 컴포넌트를 변경 대상 컴포넌트로 등록해 배치 처리 작업에서 기존에 만든 Virtual DOM과 비교해 변경된 부분이 있는지 확인한다. 이후는 setState() 메서드와 마찬가지로 "Virtual DOM 갱신 유형"에서 설명하는 순서로 진행된다.

Virtual DOM 갱신 유형

첫 번째 유형은 ReactComponent 객체의 상태가 변경될 때 하위에 있는 ReactComponent 객체를 새로 만들지 않고 속성만 갱신하는 것이다. 두 번째 유형은, 비교하는 ReactComponent 객체가 다르다면 변경된 Virtual DOM의 마운트를 해제하고 모두 새로 만들어 갱신하는 것이다.

ReactCompositeComponent 객체는 ReactCompositeComponent.updateComponent() 메서드에서 componentWillReceiveProps() 메서드, shouldComponentUpdate() 메서드, componentWillUpdate() 메서드의 순서로 메서드를 호출한다. 그리고 shouldUpdateReactComponent() 메서드에서 변경 범위를 결정한다. 만약 Virtual DOM이 같다면 속성만 갱신하지만, 새로운 Virtual DOM 이거나 Virtual DOM이 다르다면 기존의 Virtual DOM의 마운트를 해제하고 새로운 Virtual DOM을 만드는 작업을 한다.

ReactDOMComponent 객체는 ReactDOMComponent.updateComponent() 메서드에서 DOM에서 변경이 필요한 부분을 갱신하는 작업을 한다. 변경이 완료된 후 ReactReconcileTransaction 객체에 등록된 배치 처리 작업(componentDidUpdate() 메서드 등)이 실행된다. componentDidUpdate() 메서드에서 다시 state를 변경했다면 ReactDefaultBatchingStrategy 객체에 등록된 Transaction 객체가 실행된다.

변경된 ReactReconciler.receiveComponent() 메서드를 호출하며, 컴포넌트 타입에 따라 ReactCompositeComponent.receiveComponent() 메서드나 ReactDOMComponent.receiveComponent() 메서드를 호출한다. ReactCompositeComponent.receiveComponent() 메서드가 호출되면 "Virtual DOM 갱신 방법"에서 설명한 갱신 과정이 진행된다. ReactDOMComponent.receiveComponent() 메서드가 호출되면 prop를 비교해 style 속성과 event 속성등을 갱신하는 작업이 시작된다.

변경된 ReactComponent 객체와 Virtual DOM 이 다르다면 기존 Virtual DOM의 마운트를 해제하고 새로운 Virtual DOM을 만드는 작업을 한다. 

이 작업을 할 때는 Virtual DOM을 새로 만드는 작업과 유사한 작업이 진행된다. 먼저 기존 Virtual DOM의 마운트를 해제하고 새로운 Virtual DOM을 생성하며, 자식 Virtual DOM이 있다면 자식 Virtual DOM을 새로 생성해 갱신한다. 이후 "Virtual DOM 렌더링"에서 설명한 작업과 같은 작업이 시작된다. 이런 방법으로 Virtual DOM을 갱신해 Virtual DOM을 관리한다. 

Virtual DOM 갱신 작업

 

Transaction 객체

React의 작업의 대부분 바로 실행되기보다는 배치 처리로 실행된다. 그래서 React는 배치 처리를 쉽게 하기 위해 Transaction 객체를 사용해 배치 처리 작업을 패턴화했다.

다음은 Transaction 객체 코드의 주석이다.

 

알고 있어야 하는 문제

React에서는 실제 DOM을 제어하지 않고 Virtual DOM을 제어하기 때문에 DOM과 Virtual DOM 사이의 동기화를 최소화하는 것이 중요하다. 효과적으로 동기화하는 방법을 알아보자.

Life Cycle에서의 갱신

Life Cycle의 메서드인 componentWillMount() 메서드, componentDidMount() 메서드, componentWillUpdate() 메서드, componentDidUpdate() 메서드가 컴포넌트에서 실행될 때 state를 변경하면 변경 작업이 바로 반영되는 것이 아니다. 해당 컴포넌트를 변경 대상 컴포넌트로 등록하고 Virtual DOM을 갱신하는 배치 처리가 다시 시작된다.

이 작업은 고비용 작업이기 때문에 componentWillMount() 메서드와 componentDidMount() 메서드에서 state를 변경하는 것에 대해서 고민해야 한다. 만약 componentWillMount() 메서드에서 state를 변경해야 한다면 컴포넌트의 생성자에서 처리하는 것이 좋다. 그리고 componentDidUpdate() 메서드에서 Virtual DOM을 갱신한다면 무한 루프에 빠질 수 있으니 조심해야 한다.

이벤트 리스너의 변수 저장

ReactDOMComponent._updateDOMProperties() 메서드에서 변경된 사항을 적용할 때는 strict equality 방식으로 비교한다. 만약 이벤트를 인라인으로 생성해 등록하면 비교할 때 같은 함수라도 레퍼런스가 다르기 때문에 변경이 있다고 인식한다. 그래서 배치 처리 작업에 등록하고 ReactReconcileTransaction 객체가 실행된다. 이벤트에 할당하는 함수를 매번 생성하는 방법보다는 컴포넌트의 생성자에서 필요한 함수를 변수에 저장해 사용하는 것이 좋다.

이벤트 리스너를 변수에 저장하지 않고 사용한 예

render() {  
    const {id, todo, complete} = this.props;
    return (
        <li id={id} 
            onClick={() => {
                this.setState({
                    style: {
                        backgroundColor: getRandomColor()
                    }
                })
            }}
        >{todo}</li>
    );
}

이벤트 리스너를 변수에 저장해 사용하도록 개선한 예

constructor(props) {  
    super();
    this.onClick = () => {
        const {id, complete, onClick} = this.props;
        onClick({
            id : id, 
            complete : complete
        })
    };
}
render() {  
    const {id, todo, complete} = this.props;
    return (
        <li id={id} 
            onClick={this.onClick}
        >{todo}</li>
    );
}

 

shouldComponentUpdate() 메서드

컴포넌트를 갱신해야 하는지 개발자가 결정하는 메서드이다. React에서는 동기화 작업이 자주 발생하는데, shouldComponentUpdate() 메서드를 어떻게 설정하냐에 따라 불필요한 비교 횟수를 줄일 수 있다. 크게 두가지 전략이 있다.

첫 번째 전략은 빠르게 비교하기다.

빠르게 비교하기는 모든 속성에서 어떤 부분이 변경이 됐는지 확인해 컴포넌트의 갱신 시점을 판단하는 방법이다. 그러나 만약 state의 변경이 자주 발생한다면 오브젝트의 깊이 등으로 인해 변경을 확인하는 데 많은 시간이 걸릴 수 밖에 없다. 다음은 shouldComponentUpdate()에서 비교하는 방법을 개선한 사례다.

  • Redux의 connect() 메서드 : 속성이 변경될 때 변경된 부분을 확인한다.
  • Immutable Data, ECMAScript 6의 spread operator : 컴포넌트의 reference만 비교한다.
  • React.PureComponent 객체 : 키 기반으로 빠르게 컴포넌트를 비교한다.
  • Redux의 reselect 라이브러리 : memoization 기술을 적용해 특정한 파라미터는 같은 값을 반환하도록 지정해 불필요한 비교를 막는다.

두 번째 전략은 적게 비교하기다.

적게 비교하기는 변경이 잦은 컴포넌트의 깊이를 줄이는 방법이다. 어떤 컴포넌트가 변경됐다면 이를 기준으로 하위에 있는 컴포넌트를 비교하는 작업이 진행된다. 그래서 변경이 잦은 컴포넌트가 있다면 이 컴포넌트의 깊이를 줄이는 것도 성능 개선의 주요 사항이 된다. 다음은 shouldComponentUpdate() 메서드를 적게 호출해 개선한 사례다.

  • Refactoring Components : 변경이 잦은 컴포넌트의 깊이를 줄인다.

 

이벤트 시스템

React의 이벤트 시스템은 네이티브 이벤트 시스템이 아닌 자체적인 이벤트 시스템으로 구현되어 있다. React의 자체 이벤트 시스템의 작동 방법을 이해하고 작성하는 코드에 어떤 영향이 있는지 알아보자.

이벤트 할당 시스템

React에서는 렌더링할 때 컴포넌트에 어떤 이벤트가 prop으로 설정되어 있는지 확인하고 prop으로 등록된 이벤트를 등록한다. 이때 prop으로 등록된 엘리먼트가 아니라 document 엘리먼트에 이벤트를 등록한다. 일부 엘리먼트(input, option, select, textarea)의 경우에는 prop에 이벤트를 등록하지 않아도 자동으로 document 엘리먼트에 이벤트를 등록한다. 각 이벤트는 자체적으로 구축된 listenerBank 객체에 이벤트를 추가한다.

jQuery가 특정 엘리먼트를 기준으로 event delegate 기법을 사용해 이벤트를 등록했다면, React는 document 엘리먼트에 모든 이벤트를 할당하고 event delegate 기법을 사용해 처리한다고 생각하면 된다.

이벤트 처리 시스템

이벤트를 처리하는 로직은 event delegate 기법과 크게 다르지 않다. 상위 엘리먼트(document 엘리먼트)에서 이벤트를 받아 해당 이벤트가 발생해야 하는 엘리먼트에 등록된 리스너를 호출하는 기능이다. React에서 어떻게 작동하는지 좀 더 자세히 살펴보겠다.

React에서 모든 이벤트를 document 엘리먼트에 할당한 리스너가 이벤트를 받는다. 리스너에서는 네이티브 이벤트의 대상 엘리먼트를 기반으로 ReactComponent 객체를 찾는다. 초기에 등록한 이벤트 플러그인에서 분석한 이벤트를 배치 처리 작업으로 등록한다. 이벤트 분석이 끝나면 이벤트를 실행한다. 

 

출처 : https://d2.naver.com/helloworld/9297403#ch1-2-2

반응형