리액트, 제대로 알고 개발하기 pt.2 ( Hooks는 쓰레기? )

김민기·2020년 8월 19일
2

엥, 그럼 훅스는 완전 쓰레기 아니야??

" 여러분, 궁금한게 있는데요 " 동아리에서 함께 React를 공부 했던 사람들이 모여 있는 톡방에서 오랜만에 말을 꺼냈다.

앞선 포스팅을 작성 한 뒤로, 개발을 하는 데 있어 사소하지만 전반적인 변화가 생기기 시작했다. Reconciliation, Diff Algorithm 등에 대해서 공부하며 React 의 이해도가 높아짐에 따라, 코드를 작성하는데 있어 한결 자신감도 붙었다.

동아리 톡방에서 React hooks에 대한 문제의식을 나서서 제시한 것도 예전과 달라진 점 중 하나였다. 보통은 누군가 개발과 관련된 얘기를 꺼내면 아는 내용만 겨우 얘기할 때가 많았기 때문이다. 그렇기에 당연히 다들 내 궁금증에 대한 답은 알고 있을거라 생각했었으나, 질문에 대한 반응은 의외였다. ( 엥, 그럼 훅스는 완전 쓰레기 아니야?? )

그래서 내가 생각한 문제의식이 뭐냐 하면, 'React Hooks는 불필요한 리렌더링이 많이 발생한다'는 것이다. 이에 대한 답을 찾기 위해 공부한 내용을 정리하고자 이 글을 적기 시작했다.

React Hooks란?

React Hooks란 함수형 컴포넌트에서도 상태와 라이프 사이클 관련 기능을 사용할 수 있게 도와주는 기능이다.

리액트에서는 두 가지 방법으로 컴포넌트를 정의할 수 있는데 클래스형 컴포넌트와 함수형 컴포넌트가 그것들이다. 클래스형 컴포넌트는 상태를 가지고 있어, 상태 변화에 대한 관리와 라이프 사이클 관련 기능을 사용할 수 있었고, 함수형 컴포넌트는 그러지 못해 주로 화면을 그리는 용도로만 사용되었다.

그랬던 함수형 컴포넌트가 Reack Hooks의 도입 이후로 사용성이 확장된 것이다. 그렇다면, React는 왜 클래스형 컴포넌트가 이미 있음에도 불구하고 Hooks 라는 기능을 만들게 된 것 일까?

Hooks의 등장배경

리액트 공식문서상에 언급된 이유는 크게 3가지로, 클래스형 컴포넌트가 가지는 한계점들과 연관이 있다.

컴포넌트 사이에서 상태와 관련된 로직을 재사용하기 어렵다

클래스형 컴포넌트는 상태를 가지고 있는 재사용 가능한 로직을 공유하기 위해서 여러 패턴들을 이용해왔다. HOC와 render props가 그것들이다. 이러한 패턴을 사용하더라도 컴포넌트를 재구성해야 하는 점과, 코드를 추적하기 어렵게 만든다는 불편함들이 존재했다.

하지만, Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화할 수 있고, 이는 독립적인 테스트와 재사용이 가능해, 계층 변화 없이도 상태 관련 로직을 재사용 할 수 있도록 돕는다.

복잡한 컴포넌트들은 이해하기 어렵다

간단하게 시작한 컴포넌트가 점점 방대해지는 경우가 자주 있고, 이는 이해하기가 점점 더 어려워진다. Hook을 이용하면 생명주기 메서드를 기반으로 쪼개는 것이 아니라, 로직에 기반을 둔 보다 작은 함수로 컴포넌트를 나눌 수 있게 된다.

Class는 사람과 기계를 혼동시킨다.

Class는 코드의 재사용성과 코드 구성을 좀 더 어렵게 만들 뿐만 아니라, React를 배우는데 큰 진입장벽이 된다. 또한, Class는 최적화를 적용하는 과정에서, 이를 방해하는 몇몇 상황이 관찰되었다. 리액트 팀은 코드가 최적화 가능한 경로에서 유지될 가능성이 더 높은 API를 제공하려고 하는 것이다.

🖐 잠깐!

React Hooks를 소개하는 공식 문서에는 다음과 같은 인상깊은 문구가 있다. "Hooks embrace functions, but without sacrificing the practical spirit of React." (훅스는 함수를 수용하지만, 리액트의 실용적인 정신을 희생하지 않는다.)

즉, 당연하게도 훅스는 기존의 리액트 철학을 그대로 따르고자 한다.

문제의 useState()

이러한 맥락에서 내가 이상함을 느낀 것이 상태를 관리하는 함수 useState() 이다. 이에 대응되는 클래스형 컴포넌트에서의 함수는 this.setState() 함수이다. 클래스형 컴포넌트에서 상태는 객체로 관리되어지기 때문에 여러 상태를 변화시킨다고 하더라도, 한번에 상태를 변화시킬 수 있었다.

this.state = {
	a: 'a',
	b: 'b'
}
this.setState({ 
	a: 'newA',
	b: 'newB'
})

하지만, Hook에서는 각각의 상태에 대해서 따로 setState 함수가 존재하여 이에 대해서 관리하게 된다.

const [a, setA] = useState('a');
const [b, setB] = useState('b');
setA('newA');
setB('newB');

그게 왜 문제인데?

리액트는 컴포넌트의 State가 변화하면 해당 컴포넌트를 리렌더링한다는 사실을 간과해서는 안된다. 이해를 쉽게 돕기 위해, 간단히 도식화해서 그림을 그렸다. a 와 b 라는 상태값을 가지는 DOM 4개에 대해서 상태값을 변화시키는 과정이다.

React에서는 한번에 상태를 업데이트 해줌으로써 4개의 DOM이 한번에 변화한다. Virtual DOM의 도움으로 4개의 DOM의 변화는 한 번의 리렌더링으로 적용될 것이다.

반면, React Hooks 에서는 상태를 두 번에 걸쳐 변화시킨다. 그렇기 때문에 리렌더링이 일어나는 횟수도 2번이다.

VanillaJS 를 이용해서 DOM을 조작하는 경우는 한번에 4개의 DOM을 변화시킬 수도 있지만, 단순하게 하나씩 조작을 하는 경우를 그렸다. 이 경우에는 4번의 리렌더링이 일어날 것이다.

즉, 동일한 변화라도 N개의 상태가 변화하면 Hooks에서는 N배 더 많은 리렌더링이 일어나게 되는 것이다. 이는 Virtual DOM을 이용해 비교적 무거운 행위인 리렌더링의 횟수를 줄이며 mutate가 아니라 rendering을 하고자 했던 초기 React 의도에 어긋나는 것이 아닌가 하는 생각이 들었던 것이다.

그럼 어떻게해 🤔

이를 해결하기 위해 내가 적용시켜 본 방법은 2가지가 있다.

상태를 객체로 관리

클래스형 컴포넌트에서 그랬던 것 처럼 상태를 하나의 객체로 묶어서 관리하는 방법이다.

const [state, setState] = useState({
	a: 'a',
	b: 'b'
});
setState({
	a: 'newA',
	b: 'newB'
});

실제 코드 상에서 상태 3개를 하나의 객체로 묶어서 적용시켜 보았을 때, 렌더링의 횟수가 줄어드는 것을 확인할 수 있었다

개별적인 상태로 관리한 경우

하나의 객체로 관리한 경우

다음과 같은 방법으로 쓰면, 상태를 업데이트 할 때 마다 모든 상태를 써주지 않아도 된다.

setState(prevState => { return {...prevState, a: newnewA} });

useReducer

첫 번째 방법과 비슷하지만, useReducer를 사용하면 this.setState 와 보다 비슷하게 사용할 수 있다.

const [state, setState] = useReducer(
	(state, newState) => ({ ...state, ...newState}),
	{
		a: 'a',
		b: 'b'
	}
);
setState({
	a: 'newA',
	b: 'newB'
});
setState({ a: 'newA' });

변화가 생기는 상태에 대한 변화만 써줘도 다른 상태는 유지되면서 원하는 변화만 적용시킬 수 있다. 이 경우에도 렌더링 횟수는 한 번으로 줄어든다.

🤭!? React Hooks의 일괄 처리

React Hooks에서 useState가 이런 식으로 작동하게 된 이유를 계속 궁금해하며 이 글을 작성하는 도중 한 가지 충격적인 사실을 알게 되었다.

React Hooks 에서 하나의 이벤트 안에 setState가 여러 번 호출되면 이를 일괄적으로 처리해서 한 번에 반영한다는 것이다. 단, 동기 이벤트에 한하여 말이다. 즉, 내가 이상하게 여기며 관찰 했던 부분은 API Call이 일어나는 비동기 작업이 실행되는 곳이기 때문에 각각이 따로 실행 되었던 것이다.

정리하자면, 동기 작업에서는 하나의 이벤트 안에서 생긴 여러 개의 setState는 한 번에 반영이 되고, 비동기 작업에서는 개별적으로 반영된다. 그렇기 때문에, 이 경우에는 상태를 하나의 객체로 묶어서 관리하거나, useReducer를 사용하는 방법 등을 통해서 렌더링 수를 줄여줄 수 있다.

마무리

React Hooks가 비동기 작업에서 setState가 개별적으로 실행되게 둔 이유는 먼저 변화된 상태 값이 다른 상태 값의 변화에 개입되어야 하는 경우를 고려한 것 같다.

나는 리액트를 배우기도 전에 리액트 훅을 먼저 배웠다. 당시 클래스에 대한 개념이 부족했던 나에게 리액트는 이해하기가 어려웠기 때문이다. 그렇기에, Hooks에 대한 왠지 모를 애정이 있는 나였다. 그래서, 처음 React Hooks가 렌더링 횟수를 더 늘린다는 사실을 발견했을 때는 놀라움 반, React Hooks에 대한 실망 반인 심정이었다.

하지만, 동기 작업의 경우에는 React Hooks 내부에서 일괄 처리를 한다는 사실을 알게 된 뒤로는 약간의 안도감이 생겼다. 또한 비동기 작업의 경우에도 원하면 하나의 객체로 관리해주는 방법을 사용하면 불필요한 렌더링 횟수를 줄일 수 있다는 사실을 알게 되었다.

참고문헌

https://velog.io/@youthfulhps/React-Hooks-1-Hook의-등장-배경
https://ko.reactjs.org/docs/hooks-intro.html
https://dev-momo.tistory.com/entry/React-Hooks
https://stackoverflow.com/questions/53574614/multiple-calls-to-state-updater-from-usestate-in-component-causes-multiple-re-re
https://github.com/facebook/react/issues/14259

profile
봄 날을 기다리는 눈사람

0개의 댓글