React 는 개발자가 정말로 이해하기 쉽게 Components 들을 생성하고 이를 사용할 수 있도록 되어있는 라이브러리다. 하지만 구체적으로 내부에서 어떻게 동작 하는지는 직접 찾아보지 않는 한 이해하기가 어려울 것이다. 최적화를 이해하기 위해서는 Element, Component, Instance 에 대한 이해가 선행되어야 한다.
Element (React Element)
React 앱의 가장 작은 단위.
브라우저 DOM 엘리먼트와 달리 일반 객체(plain object) 이며 불변객체다. 실제 사용되는 인스턴스가 아니라 단지 UI 를 표현하는데 사용되는 객체다.
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
children: 'OK!'
}
}
} // element. element 는 type 과 props. 단지 두개의 fields 를 가진다.
// type: string | Component
// props: Object
JavaScript
복사
// 위 엘리먼트는 HTML 을 plainObject 로 표현한 것일 뿐!
<button class='button button-blue'>
<b>
OK!
</b>
</button>
HTML
복사
React DOM 은 React 엘리먼트와 일치하도록 DOM 을 업데이트한다. 모두 변경하는 것이 아니라 이전 객체와 비교해서 DOM 에서 변경되어야할 부분만 변경시킨다.
위의 전제를 바탕으로 결론을 내자면 UI 를 업데이트 하는 유일한 방법은 새로운 엘리먼트를 생성하고 이를 DOM 에 rendering 할 수 있도록 전달하는 것이다. render 함수는 이 엘리먼트(Plain Object)를 생성하는 함수다.
Component
Element 의 type 이 string 일 경우, HTML DOM 의 tag name 으로 대응이 되지만, type 은 react 의 컴포넌트를 표현하는 function, 혹은 class 가 될 수도 있다. 이 부분이 react 의 핵심 아이디어다.
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
JavaScript
복사
DOM node 를 표현하는 element 와 똑같이 Component 를 표현한다.
React 에서는 type 이 function, 혹은 class 인 element 를 보면 해당 컴포넌트에게 주어진 props 로 어떤 element 를 렌더링하는지 물어볼 것이다. (Refine 작업)
function Button() {
return (<button className={'button button-blue'}><b>OK</b></button>);
}
{
type: Button,
props: {
}
}
// React will ask to this element 'what will you rendering'=>
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
children: 'OK!'
}
}
}
JavaScript
복사
React 는 위 Refine 작업 과정을 element 의 타입이 DOM tag string type 이 나올 때 까지 반복할 것이다.
class Button extends React.Component {
render() {
const { children, color } = this.props;
// Return an element describing a
// <button><b>{children}</b></button>
return {
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
};
}
}
JavaScript
복사
결론
React 에 있어서 props 는 input, element tree 는 output 이다
Instance
DOM node 에 대한 참조, 혹은 children components 에 대한 참조를 유지, 변경, 삭제 등을 할 수 있다.
사실 우리가 React 를 프로그래밍 함에 있어서 instance 는 그리 중요하지 않다.
(DOM 에 대한 인스턴스가 아니라) React Instance 의 경우 class 를 통해 만들어진 Component 일 경우에만 생성이 된다. (그래서 state 가 생성 및 유지, 변경이 될 수 있는 것이다.)
React's work
•
랜더링 단계: 변경 해야할 사항을 결정한다. render 를 호출 한 다음 그 결과를 이전 렌더링과 비교(diffing)함.
•
커밋 단계: 변경 사항을 적용하는단계. React DOM 의 경우 DOM Node 들을 삭제 및 업데이트, 생성 등을 처리. Reat 는 이 단계에서 componentDidMount 등과 같은 생명 주기 함수들을 호출한다.
Reconciliation (랜더링 단계)
리액트에서는 React element tree 이용해서 현재 Element tree 와 변경될 Element tree 를 비교 (diffing 알고리즘)해서 실제 DOM 을 업데이트 시킨다. 그렇다면 diffing 에서 어떤 사항들을 변경으로 여기고 이를 업데이트 시킬까? 한 번 알아보자!
React 에서는 Reconciliation 작업을 선행하기 때문에 UI 에 대한 제어를 최소화시키는 것이다 (일반적으로 UI 제어 비용이 상대적으로 더 비싸다.) React 의 diffing 알고리즘은 휴리스틱 알고리즘으로 다음과 같은 조건을 통해서 O(n)에 근사할 수 있도록 구현하였다.
1.
다른 타입의 두 엘리먼트는 다른 트리를 생성한다.
2.
각 렌더링에서 유지되는 엘리먼트에 key 프로퍼티를 통해서 같은 엘리먼트라는 것을 알린다. (같은 레벨에서만 유효 )
*** 기본적으로 서브 트리들의 위치를 기준으로 비교함
Elements of diffrent types
같은 위치에서 엘리먼트의 타입이 다른 경우
1.
기존 트리를 제거 후 새로운 트리 만든다.
2.
기존 트리 제거시 트리 내부의 엘리먼트/컴포넌트들은 모두 제거한다.
3.
새로운 트리를 만들 때 내부 엘리먼트/컴포넌트들도 모두 새로 만든다.
DOM Elements Of The Same Type
같은 위치에서 엘리먼트가 DOM 을 표현하고 그 타입이 같은 경우
1.
엘리먼트의 attributes를 비교한다.
2.
변경된 attributes만 업데이트한다.
3.
자식 엘리먼트들에 diff 알고리즘을 재귀적으로 적용한다.
Component Elements Of The Same type
같은 위치에서 엘리먼트가 컴포넌트를 표현하고 그 타입이 같은 경우
1.
컴포넌트 인스턴스 자체는 변하지 않는다.(때문에 컴포넌트의 state가 유지된다.)
2.
컴포넌트 인스턴스의 업데이트 전 라이프 사이클 메서드들이 호출되며 props가 업데이트된다.
3.
render()를 호출하고, 컴포넌트의 이전 엘리먼트 트리와 다음 엘리먼트 트리에 대해 diff 알고리즘을 재귀적으로 적용한다.
Recursing On Children
기본적으로 자식 엘리먼트들에 대해 반복적인 비교를 할 때, React는 이전/다음 상태의 자식 엘리먼트 목록을 함께 반복하고 그 차이를 본다. 따라서 엘리먼트들의 정렬과 같은 상황에 취약하다.
{/* Before */}
<ul>
<li>first</li> {/* prev-first */}
<li>second</li> {/* prev-second */}
</ul>
{/* After (with reordering) */}
<ul>
<li>second</li> {/* Compares prev-first --> Update dom */}
<li>first</li> {/* Compares prev-second --> Update dom */}
<li>third</li> {/* Compares prev --> Insert dom */}
</ul>
JavaScript
복사
Keys
엘리먼트들에게 Key 속성을 명시적으로 부여하여 위와 같은 상황에 발생하는 필요 없는 업데이트를 최소화시킬 수 있다.
{/* Before */}
<ul>
<li key="first">first</li> {/* prev-first */}
<li key="second">second</li> {/* prev-second */}
</ul>
{/* After (with reordering) */}
<ul>
<li key="second">second</li> {/* Compares prev-second --> Update X, Reorder dom */}
<li key="first">first</li> {/* Compares prev-first, --> Update X, Reorder dom */}
<li key="thrid">third</li> {/* Compares prev --> Insert dom */}
</ul>
{*/ But!!! only same level! */}
JavaScript
복사
그렇다면 최적화는 어떻게?
기본적으로 React 에서는 UI 제어를 최소화 시키기 때문에 기본적으로 빠르다고 할 수도 있지만 제어를 최소화 시키기 위해서 선행되는 작업들의 비용도 무시할 수 없다.
(React Element Tree 에서 특정 Component 들을) Refining 작업과 diffing 작업을 skip 하는 것!
만약 props 가 immutable 이라면 props 가 변경되었는지 체크하는 것은 성능적으로 큰 이점이 있다. React 에서 immutability 가 핵심인 이유!
shouldComponentUpdate
컴포넌트가 렌더링 전에 호출하는 (React.Component 에 내장된) 라이프사이클 메서드. return 하는 boolean 값을 통해서 true 라면 리렌더링, false 를 return 하면 리렌더링을 하지 않도록 할 수 있다. shouldComponentUpdate 는 기본적으로 항상 return true 로 구현되어 있기 때문에 override 를 통해서 구현 하면 된다. 만약 shouldComponentUpdate 함수에서 return false 를 했다면 render 함수도 실행이 되지 않는 것이다!
c8 의 경우 scu 에서 true 반환했기 때문에 엘리먼트를 렌더링 하지만, 이전 상태의 엘리먼트와 변경된 다음의 엘리먼트가 차이가 없기 때문에 DOM 업데이트를 하지 않는다. 이런 경우 React 의 렌더링 과정은 불필요하고, 성능 저하를 유발 했다고 볼 수 있다.
PureComponent
React.PureComponent는 shouldComponentUpdate API를 제외하고 React.Component와 같다. PureComponent는 renderer에서 shouldComponentUpdate 라이프사이클 로직을 수행할 때 기본적으로 shallow-compare를 수행한다.
PureComponent 에서 shouldComponentUpdate 를 작성하는 것은 PureComponent 의 구현을 무시하는 것이기 때문에 작성하지 않아야함!
functional component 일 경우 React.memo 사용
고차 컴포넌트.(HOC)
결과를 메모이징하여 만약 동일한 props 를 입력한다면 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용.
const MyComponent = React.memo(function MyComponent(props) {
/* props를 사용하여 렌더링 */
});
JavaScript
복사
React.memo는 props 변화에만 영향. React.memo로 감싸진 함수 컴포넌트 구현에 useState 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링이 될 것.
기본적으로 shallow-compare이나, 두번째 인자로 areEqual 함수를 입력할 수 있음.
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProp가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
JavaScript
복사
Mobx 에서는?
mobx-react
observer
HOC.
React Component 를 reactive 한 컴포넌트로 변환하는 함수(or decorator). 변환된 컴포넌트는 rendering 에 사용되는 모든 observable 한 값들을 트래킹할 것이고, 자동으로 이 값들이 변하면 re-rendering 을 한다. rendering 에 사용되지 않은 값들이 변경될 경우에는 re-render 가 되지 않는다.
Functional Component observer 할 경우
React.memo 가 자동으로 적용됨
Class Components
shouldComponentUpdate 는 지원되지 않음. 때문에 PureComponent 를 상속하는것이 권장됨. observer 는 객체에서 값을 dereference 할 때 반응(react)하기 때문이다!
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increaseTimer() {
this.secondsPassed += 1
}
}
const myTimer = new Timer()
// A function component wrapped with `observer` will react
// to any future change in an observable it used before.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
setInterval(() => {
myTimer.increaseTimer()
}, 1000)
JavaScript
복사
최적화를 위해선!
1. obervable 한 값을 읽는(read) 모든 컴포넌트에 observer 를 사용한다.
observer 는 컴포넌트를 향상시키는 곳에만 사용되는 함수일 뿐. observer로 모든 컴포넌트를 감싸면 대부분 최적화는 자동으로 된다!
2. dereference 를 최대한 늦게 한다.
가능한 오랫동안 객체(및 배열 등등) 참조를 전달하고 DOM 을 랜더링하는 component 와 같이 low-level component 내에서 해당 property 들을 read 한다.
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerViewer secondPassed={myTimer.secondsPassed} />, document.body)
// 위와 같이 구현되어 있다면, TimerView 는 React 하지 않을 것이다
JavaScript
복사
3. observable 한 값을 observer 로 감싸지 않은 컴포넌트에 전달하지 말자! = 그냥 무조건 observer 로 감싸자 (1번 규칙)
ETC
Virtualize long lists
만약 아주 많은 row 를 포함한 list 데이터를 표현해야 한다면, windowing 이라고 알려진 기술을 사용하자!
windowing = 수 많은 row 들 중 일부의 row 들만 렌더링 하는 것. 실제 DOM 노드들을 생성하는 숫자와 컴포넌트들을 리렌더링 하는 시간들을 극적으로 줄여줌.
react-native 에서는 VirtualizedList, FlatList, SectionList 를 사용하면 된다!
Component 분리
컴포넌트가 적절히 분리되지 않으면 가독성, 유지보수에만 문제가 있는 것이 아니라 성능적으로도 손실을 볼 수 있다.
class App extends Component {
render() {
return (
<div className="app">
...
<div className="app-intro">
{this.state.title}
</div>
<div className="list-container">
<ul>
{
this.state.items.map((item) => {
return <Item key={item.id} {...item} />
})
}
</ul>
</div>
</div>
)
}
}
JavaScript
복사
위 상태에서 title 을 변경하면 어떤 일이 생길까?
title 만 변경되는 모든 DOM element 에서 render 가 실행된다. 하지만 ul 및 그 하위 lists 들은 불필요한 렌더링이 실행된 것이다. 때문에 ul 및 list 들을 따로 컴포넌트로 빼서 PureComponent 로 생성한다면 title 이 변경 되었을 시 List 부분은(불필요한 부분은) 렌더링이 실행되지 않을 것이다.
render() {
<div className="app">
...
<div className="app-intro">
{this.state.title}
</div>
<List items={this.state.items} />
</div>
}
JavaScript
복사
send appropriate props
컴포넌트를 적절히 분리하고, PureComponent를 사용해도 여전히 의도치 않은 성능 하락을 일으킬 수 있다.
render() {
return (
<div className="app">
...
<div className="app-intro">
{this.state.title}
</div>
<List items={this.state.items} deleteItem={id => this.deleteItem(id)}/>
</div>
);
}
JavaScript
복사
위 코드에서 List 컴포넌트가 PureComponent 로 구현되어 있다 해도 List 컴포넌트는 re-render 될 것이다. 이는 바로 deleteItem property 때문이다. App component 에서 render 함수를 실행할 때마다 deleteItem 에서 새로운 함수를 생성할 것이기 때문에 (shallow-compare에서 false 가 나올 것) render 함수가 실행될 것이다.
때문에 props 에서 넘어가는 함수는 생성자에서 미리 바인딩하고 새로운 함수를 생성하지 않고 전달하는 것이 일반적으로 성능 이슈에 좋다. (ex - deleteItem={this.deleteItem})
Production Build!
일반적으로 React 에는 개발할 때 도움이 되는 warning 기능들을 많이 포함하고 있다. 하지만 이 기능들은 production 에서는 불필요하며, React Project 를 거대하게 만들기 때문에, production build 를 통해서 app 을 deploy 해야한다.
build 는 여러가지 툴로 가능하다
•
create react app
•
brunch
•
browserify
•
rollup
•
webpack
•
......
ETC
JSX
마크다운 표현식을 통해서 Element 를 표현 가능하도록 함.
Babel은 JSX를 React.createElement() 호출로 컴파일.
const element = ( // JSX
<h1 className="greeting">
Hello, world!
</h1>
);
// same both
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
JavaScript
복사
React Profiler (in react devtool)
commit 단계에 대한 정보를 제공한다.