Virtual DOM
브라우저 DOM에서 10개의 변경사항이 발생하면, 10번을 리렌더링한다. 브라우저 렌더링에서 layout(배치)과 paint(그리기)는 시간이 많이 걸리는 연산이며 변경사항이 발생할 때마다 계속해서 리렌더링하므로 속도도 느리고 메모리 낭비도 크다.
그래서 리액트에서는 Virtual DOM(가상 DOM)을 만들어 변경된 부분을 모아 실제 DOM에 업데이트 한다. 어플리케이션에서 변경사항이 발생하면 Virtual DOM의 전체 UI가 리렌더링 되며 이전 DOM과 새로운 DOM의 차이를 체크한다. 그 다음 실제 DOM에 변경된 부분만 업데이트한다. 여러 개의 변경사항이 발생해도 이를 모아 한 번만 업데이트하기 때문에 렌더링 속도를 높이고, 메모리 낭비도 줄일 수 있다.
HOC(Higher Order Component)
자바스크립트의 HOF(Higher Order Function)처럼 HOC는 컴포넌트를 인수로 받아 컴포넌트를 반환하는 함수이다. HOC는 원본 컴포넌트를 변경하지 않고 조합하여 반환하는 함수로서 이는 *순수 함수의 일종이다.
*순수 함수(pure function): 함수 내부에서 외부 데이터에 직접 접근하지 않고 파라미터로 받아서 사용한다. 때문에 원본 데이터는 불변성을 유지할 수 있다. 단, 외부 데이터라 해도 변경 가능성이 없다면(ex. Object.freeze로 동결, 스칼라 타입의 상수 등) 함수 내부에서 접근해도 불변성이 유지되므로 순수 함수와도 같다.
HOC은 여러 컴포넌트들 사이에서 비슷한 로직이 반복될 때 사용한다. 예를 들어 외부 데이터 DataSource에 접근해서 필요한 데이터를 얻고, 변경사항을 업데이트 하는 컴포넌트들이 여러 개 있다고 하자. withSubscription 이라는 HOC을 만들어서 컴포넌트와 데이터에 접근하는 함수 selectData를 인수로 받는다. HOC 내에서는 selectData를 통해 컴포넌트가 필요로 하는 데이터만 가져와 해당 컴포넌트의 props에 데이터를 전달한다.
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
JavaScript
복사
source of code : react document - HOC
HOC은 render 메서드 안, 혹은 function 컴포넌트 안에서 사용되어선 안된다. 리렌더링할 때마다 고차컴포넌트가 호출되면서 고차컴포넌트가 반환하는 컴포넌트에 변경사항이 없더라도 기존과 다른 타입의 컴포넌트로 취급받게 된다.
render () {
EnhancedComponent = withSubscription(WrappedComponent, selectData)
}
JavaScript
복사
reconciliation(재조정)은 컴포넌트의 identity를 통해 기존 서브트리를 업데이트할 지 아니면 unmount 시키고 새로운 노드를 마운트할 지 결정한다. 처음 렌더링된 EnhancedComponent와 리렌더링에서 새롭게 반환된 EnhancedComponent가 같지 않으므로 reconciliation을 통해 subtree만 업데이트하는 게 아니라 전체 컴포넌트를 새로 마운트 시켜버린다. 렌더링할 때마다 변경된 부분만이 아닌 컴포넌트의 전체 노드를 다시 마운트하므로 성능상의 문제도 있지만, state를 유지하지 못한다는 문제도 있다. 그러므로 컴포넌트 정의 바깥에서 HOC을 적용시켜야 한다.
HOC는 컴포넌트들 사이에서 반복되는 로직을 재사용하기 위해 사용한다고 했다. 마찬가지로 hook 역시 컴포넌트에서 반복되는 로직을 재사용하기 위해 사용한다. 커스텀 훅을 만들어 HOC을 사용하는 대부분의 경우를 대체할 수 있다. 훅을 만드는 것은 간단하며 HOC을 사용할 때의 중첩된 컴포넌트를 줄일 수 있다는 장점이 있다.
Reconciliation (재조정)
렌더링할 때 모든 트리를 새로 마운트하지 않고, 변경이 필요한 부분만 마운트한다. 리액트에서는 이를 위해 비교 알고리즘을 사용하여 불필요한 업데이트를 줄인다. 이 알고리즘에는 두 가지 사실을 가정한다.
1.
서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
2.
개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
UI에 변경사항이 생기면 리액트는 기존 컴포넌트와 새로운 컴포넌트의 트리를 비교한다. 비교 알고리즘은 루트 엘리먼트부터 비교하므로 아래와 같이 루트 엘리먼트가 바뀐 경우 이전 컴포넌트의 트리를 모두 버리고 새로 마운트한다. 기존의 컴포넌트를 모두 버렸기 때문에 state 역시 소실된다.
<div>
<Counter />
</div>
<span>
<Counter />
</span>
TypeScript
복사
반면, 기존 엘리먼트 타입이 같고 속성만 달라진 경우, 속성만 업데이트한다. 이때는 컴포넌트가 유지되어 리렌더링 후에도 컴포넌트의 state는 유지된다.
리액트는 루트 엘리먼트에서부터 트리를 비교하며 자식 엘리먼트를 재귀적으로 순회하며 비교한다. 이때 자식 요소의 첫번째 엘리먼트에 변경사항이 생기면 같은 레벨의 형제 엘리먼트까지 버려지고 다시 마운트될 수 있다. 그저 순서에 의해서 변경할 필요가 없는 다른 엘리먼트까지 업데이트되는 것이다. 리액트에서는 이 문제를 해결하기 위해 key 속성에 엘리먼트에 대한 id를 부여해 새로 추가된 엘리먼트와 변경이 필요없는 기존 엘리먼트를 구별한다. key로 구분하기 때문에 어떤 엘리먼트가 새로 추가되었는지 없어졌는지 판단할 수 있다.
references
•
◦
useSelector, useDispatch 와 같은 hook들이 만들어졌기 때문에 connect를 사용할 일은 별로 없다.