-
[React] React 작동 원리web study/react 2021. 7. 14. 14:10
리엑트 작동 원리에 대해 대략적으로 알고는 있었지만
리엑트가 어떻게 작동되나요 라고 물어본다면 답을 하기는 어려웠을 것 같다.
그래서 오늘은 리엑트의 작동원리에 대해 알아보도록 하겠다.
오늘은 3가지 키워드로 정리를 해보려 한다.
Virtual DOM, 리엑트의 이벤트 처리, JSX 이 3개를 정리하려 하는데 가장 중요한 Virtual DOM부터 정리해보자 한다.
Virtual DOM이란
DOM과 비슷한 React의 객체 트리다. 개발자는 DOM을 직접 제어하지 않고 Virtual DOM을 제어하게 되며, 이를 통해 DOM에 반영하는 작업을 한다.
Virtual DOM은 ReactDOM.render()함수를 호출하면 Virtual DOM을 만들기 시작하게 되는데 루트(최상단 부모 컴포넌트) component인 App 컴포넌트를 생성하는것을 말한다.
Virtual DOM이 형성되는 과정에서 라이프 사이클에 대한 이야기를 빼놓을 수는 없는데 우선 Virtual DOM이 어떤식으로 형성되는지 순서대로 알아보도록 한다.
- 최초의 root component에서 ReactDOM.render()를 감지하면 render()함수는 React.createElement(App)를 파라미터로 전달 받는다.
- render() 함수는 react에서 사용하는 타입의 컴포넌트를 생성한다.
- 컴포넌트 생성 과정에서 많은 자식 컴포넌트들을 만나게 되는데 2가지 타입으로 컴포넌트를 생성하게 된다.
- ReactCompositeComponent는 DOM이 아닌 컴포넌트를 생성하는데 사용된다. 아래는 ReactCompositeComponent 객체의 주요 작업이다.
- componentWillMount() 메서드 실행
- 렌더링 실행
- 배치 처리 작업(ReactReconcileTransaction 객체)에 메서드나 속성 등록
- componentDidMount() 메서드가 있으면 componentDidMount() 메서드 등록
- ref 속성이 있으면 attachRefs 속성 등록
- 하위 ReactComponent 객체가 있으면 ReactComponent 객체를 생성하고 다시 ReactReconciler.mountComponent() 메서드를 실행
- ReactDOMComponent는 DOM을 만들 때 생성하는 컴포넌트이다. 아래는 ReactDOMComponent 객체의 주요 작업.
- 실제 DOM을 생성
- style 속성과 attr 속성 추가
- 배치 처리 작업(ReactReconcileTransaction 객체)에 사용자 이벤트 등록
- 하위 ReactComponent 객체가 있으면 ReactComponent 객체를 생성하고 다시 ReactReconciler.mountComponent() 메서드를 실행
- 최상위 DOM(root DOM)에 DOM을 추가(현재 최상위 DOM은 document 객체에 추가되지 않은 상태)
- render() 함수가 생성한 컴포넌트를 React 컴포넌트에 마운트 하기 위해 ReactReconciler.mountComponent() 메서드를 호출한다. ReactReconciler.mountComponent() 메서드는 render() 함수가 생성한 객체의 mountComponent() 메서드를 호출하며, 이 시점에 주요 작업이 시작된다(render()함수가 컴포넌트를 생성하는 과정 위의 2가지 타입 컴포넌트 생성 과정 참고)
- App 컴포넌트가 생성되면 <div> 엘리먼트를 생성한다. <div> 엘리먼트와 같은 실제 DOM과 함께 ReactDOMComponent 객체가 생성된다.
- Header 컴포넌트와 TODOList 컴포넌트, Footer 컴포넌트가 ReactCompositeComponent 객체로 생성되는 작업이 시작된다. 자식 컴포넌트로 내려가면서 ReactDOMComponent 객체와 ReactCompositeComponent 객체의 주요 작업을 반복적으로 실행하며 Virtual DOM을 생성한다.
복잡하니 간단하게 요약하자면 App컴포넌트를 생성하는 과정에서 App 컴포넌트가 생성되면 자식 컴포넌트로 내려가면서 DOM과 관련된 객체와 Composite 객체의 주요 작업들을 반복하면서 Virtual DOM을 생성한다고 보면 될 것 같다.
Virtual DOM의 상태 갱신
Virtual DOM을 만든 후 Virtual DOM을 갱신할 때 Reconciliation 작업을 한다. Reconciliation 작업은 Virtual DOM과 DOM을 비교해 DOM을 갱신하는 작업으로 크게 2가지 방법이 있다.
- 일반적인 상태 변경
- setState() 메서드를 호출하여 해당 컴포넌트를 대상 컴포넌트로 등록해 갱신하는 방법
- 시작 기준이 대상 컴포넌트를 기준으로 한다.
- setState() 메서드를 실행하면 변경 대상 컴포넌트를 ReactUpdates.enqueueUpdate() 메서드에서 등록한다. 등록된 변경 대상 컴포넌트는 ReactDefaultBatchingStrategy.close() 메서드에 등록한 ReactUpdates.flushBatchedUpdates() 메서드에서 갱신한다.
- store를 이용하는 상태 변경
- 스토어가 변할 때 최상의 컴포넌트의 render()함수를 호출해 최상위 컴포넌트를 변경 대상 컴포넌트로 등록하는 방법
- 시작 기준이 root 컴포넌트를 기준으로 한다.
위와 같은 2가지 갱신 방법은 시작 기준만 다르고 그 이후에 비교하는 방법은 같다.
Virtual DOM 갱신 유형
갱신 유형에는 2가지가 존재한다.
- ReactComponent 객체의 상태가 변경될 때 하위에 있는 ReactComponent 객체를 새로 만들지 않고 속성만 갱신하는 것
- 비교하는 ReactComponent 객체가 다르다면 변경된 Virtual DOM의 마운트를 해제하고 모두 새로 만들어 갱신하는 것
상태가 변경되는 경우 ReactCompositeComponent 객체와 ReactDOMComponent 객체가 각각 상태를 갱신하기 위한 작업을 진행하게 되며 Virtual DOM이 이전과 같다면 속성만 갱신하지만, 새로운 Virtual DOM이면 기존의 Virtual DOM의 마운트를 해제하고 새로운 Virtual DOM을 만드는 작업을 진행한다.
Virtual DOM이 새로 생성되는 과정은 먼저 기존 Virtual DOM의 마운트를 해제하는 것으로 시작된다.
마운트가 해제되면 새로운 Virtual DOM을 생성하며, 자식 Virtual DOM이 있다면 자식들도 새롭게 생성하여 갱신한다.
이벤트 처리
리엑트의 이벤트 시스템은 네이티브 시스템을 따르지 않고 자체적인 이벤트 시스템으로 구현되어 있다.
리엑트에서는 렌더링할 때 컴포넌트에 어떤 이벤트가 prop으로 설정되어 있는지 확인하고 이벤트를 등록한다.
이 때 prop으로 등록된 엘리먼트가 아니라 document 엘리먼트에 이벤트를 틍록한다.
이벤트를 처리하는 로직은 document에서 이벤트를 받아 해당 이벤트가 발생해야 하는 엘리먼트에 등록된 리스너를 호출하는 방식을 사용한다.
모든 이벤트는 리스너가 이벤트를 받으며 리스너에서는 타겟 엘리먼트를 기반으로 ReactComponent 객체를 찾는다. 초기에 등록한 이벤트 플러그인에서 분석한 이벤트를 배치 처리 작업으로 등록하고 이벤트 분석이 끝나면 이벤트를 실행하게 된다.
리엑트는 실제 이벤트가 아닌 가상으로 만든 React Event 객체를 사용하는데 이벤트 전파와 기본 기능을 막을 수 있으며, 이벤트가 발생되면 바로 이벤트 연결을 해제한다.
리엑트의 이벤트 처리 과정 리엑트의 이벤트 처리 과정에서 문제가 발생할 수도 있는데 이벤트 전파, 스크롤 성능 저하, object pool 방식의 문제들이 있다.
- 이벤트 전파
- 이벤트 전파는 리엑트로 만들어진 영역을 DOM을 컨트롤 하는 코드에서 발생하게 되는데 가급적 함께 사용하지 않아야 한다.
- 리엑트는 document 엘리먼트에 버블링으로 이벤트를 할당하기 때문에 DOM을 직접 제어하는 코드와 함께 같은 영역에 사용하면 DOM을 직접 제어하는 이벤트 핸들러에서 stopPropagation() 메서드를 호출했을 때 react로 이벤트가 전달되지 않게 된다.
- 스크롤 성능 저하
- 모바일 웹에서 스크롤은 터치 이벤트의 영향을 받는다.
- touchstart 이벤트와 touchmove 이벤트에서 preventDefault() 메서드로 인해 scroll 이벤트의 발생 여부가 결정되기 때문에 touchstart 이벤트와 touchmove 이벤트가 끝날 때까지 scroll 이벤트는 작동하지 않는다
- 이는 리엑트 구조상 어쩔 수 없이 발생하는 문제로써 가급적 불필요한 터치 관련 이벤트를 제거하는 것이 사용자 경험을 높이는 방법 중 하나라고 할 수 있다.
- object pool 방식의 문제
- 이벤트가 발생할 때마다 react 이벤트 객체를 만들면 가비지 컬렉션이 자주 발생하는데 이를 위해 react 에서는 object pool 방식을 사용한다.
- 리엑트에서는 이벤트 콜백 함수가 호출되면 이벤트 객체의 속성을 초기화 해버린다.
- 그래서 변수에 event 객체를 할당하고 콜백 함수가 실행된 이후에 접근하면 null인 상태가 된다.
JSX
jsx는 javascript의 확장 문법이다. 쉽게 말해 HTML 문법을 javascirpt 코드 내부에 사용하는 것이라 볼 수 있다.
리엑트에서 jsx는 빌드 과정에서 Babel에 의해 자바스크립트 코드로 변환되는 과정을 거치게 되는데 이를 트랜스 파일이라고 한다.
JSX에는 사용에 대한 기본적인 규칙과 특징이 존재한다.
- 컴포넌트에 여러 요소가 있다면 반드시 부모 요소 하나로 감싸야 한다.
function JSX(){ return ( <div> <h1>부모에게 감싸져야해요!</h1> <div>저의 부모는 div에요!</div> </div> ) }
Fragment를 이용하여 감싸주는 것만 표현도 가능하다.
function JSX(){ return ( <> <h1>부모에게 감싸져야해요!</h1> <div>저의 부모는 Fragment에요!</div> </> ) }
- 표현식을 작성할 수 있다.
function JSX(){ let data = "안녕하세요?" return <h1>{data}</h1> }
- 조건부 연산자를 사용할 수 있다.
function JSX(){ let isBoolean = true return ( <div> {true ? (<div>이게 출력 될꺼에요!</div>) : (null)} </div> ) }
- undefined와 null은 랜더링 하지 않는다.
- XSS 공격으로부터 안전하다.
- React DOM은 JSX에 삽입된 모든 값을 렌더링 하기 전에 이스케이프 하므로, 애플리 케이션에서 명시적으로 작성되지 않은 내용은 주입되지 않는다.
오늘은 리엑트의 작동 원리에 대해 알아보았다.
virtual dom에 대해서 어떻게 작동되는지 대략적으로는 알고 있었지만 어떤 특징을 가지고 랜더링의 기준을 정하는지는 몰랐는데 더욱 깊게 알게된거 같아서 차후에 코드를 짤 때 보다 효율적으로 작업할 수 있을 것 같다는 생각이 들었다.
참고자료
https://d2.naver.com/helloworld/9297403#ch1-3
'web study > react' 카테고리의 다른 글
[redux] 미들웨어 (0) 2021.04.10 [react] context (0) 2021.04.03 [리엑트] Atomic folder design (0) 2021.03.27