출처 김용찬, 모던 리액트 Deep Dive, 위키북스, 2023년
오랜 기간 리액트 생태계에서는 리액트 애플리케이션의 상태 관리를 위해 Redux 리덕스에 의존했다. 그러나 현재는 Context API, useReducer, useState의 등장으로 컴포넌트에 걸쳐서 재사용하거나 컴포넌트 내부에서 상태를 관리할 수 있는 방법들이 등장하기 시작했다. 덕분에 리덕스 외의 다른 상태 관리 라이브러리를 선택하는 경우도 많아지고 있다.
1. 가장 기본적인 방법: useState
useState의 등장으로 리액트에서는 여러 컴포넌트에 걸쳐 동일한 인터페이스를 가진 상태를 생성하고 관리할 수 있게 됐다.
다음은 useCounter라는 사용자 훅이다. 상태값인 counter와 inc 함수를 객체로 반환한다. Counter1과 Counter2 컴포넌트는 카운터 기능을 위해 useCounter 훅을 사용한다. 각각의 컴포넌트는 상태값을 독립적으로 유지할 수 있다.
지역 상태
useState를 기반으로 하는 사용자 훅의 한계는 명확하다. 훅을 사용할 때마다 컴포넌트별로 초기화되므로 컴포넌트에 따라 서로 다른 상태를 가진다는 점이다. counter는 useCounter가 선언될 때마다 새롭게 초기화된다.
기본적인 useState를 기반으로 한 상태를 지역 상태 local state라고 한다. 지역 상태는 컴포넌트별로 상태의 파편화를 만든다. useCounter에서 제공하는 counter를 올리는 함수는 지금처럼 동일하게 사용하되, 두 컴포넌트가 동일한 counter 상태를 바라보게 하기 위해서는 현재 지역 상태인 counter를 여러 컴포넌트가 동시에 사용할 수 있는 전역 상태 global state로 만들어야 한다.
전역 상태
useCounter를 각 컴포넌트에서 사용하는 대신, Parent라고 불리는 상위 컴포넌트에서만 useCounter를 사용하고, 이 훅의 반환값을 하위 컴포넌트의 props로 제공하면 여러 컴포넌트가 동일한 상태를 사용할 수 있다.
2. 지역 상태의 한계 벗어나기. useState의 상태를 바깥으로 분리하기
useState의 한계는 명확하다. 현재 리액트의 useState는 리액트가 만든 클로저 내부에서 관리되어 지역 상태로 생성되기 때문에 해당 컴포넌트에서만 사용할 수 있다. 그렇다고 아무리 외부 파일에서 state 값을 바꾸려고 시도하더라도 useState를 사용하지 않으면 함수 컴포넌트에서 리렌더링이 일어나지 않는다.
함수 컴포넌트에서 리렌더링을 하려면 다음과 같은 작업 중 하나가 일어나야 한다.
1. useState, useReducer의 반환값 중 두 번째 인수가 어떻게든 호출된다.
2. 부모 함수(부모 컴포넌트)가 리렌더링되거나 해당 함수(함수 컴포넌트)가 다시 실행되어야 한다.
다음은 컴포넌트 레벨의 지역 상태를 벗어나는 새로운 상태 관리 코드이다.
이 상태는 store로 정의한다. (store.tsx라는 새로운 파일에 정의함) store의 값이 변경될 때마다 변경됐음을 알리는 callback 함수를 실행시킨다. callback 함수를 등록하는 함수는 subscribe이다. 이 store를 참조하는 컴포넌트는 subscribe에 컴포넌트 자신을 렌더링하는 코드를 추가해서 컴포넌트가 리렌더링을 실행할 수 있게 만든다.
store로 상태 관리
이 useStore도 완벽한 것은 아니다. store의 구조가 원시값이라면 상관없지만 객체인 경우, 해당 객체에서 일부값만 변경되어도 리렌더링이 일어난다. useStore에서 원하는 값이 변했을 때만 리렌더링되도록 useStore 훅을 수정하여 useStoreSelector를 만들었다. useStore를 기반으로 만들어졌지만, 두 번째 인수로 selector라는 함수를 받는다.
selector는 store의 상태에서 어떤 값을 가져올지 정의하는 함수로, 이 함수를 활용해 store.get()을 수행한다. useState는 값이 변경되지 않으면 리렌더링을 수행하지 않으므로 store의 값이 변경됐다 하더라도 selector(store.get())이 변경되지 않으면 리렌더링이 일어나지 않는다.
store로 상태 관리 - 원하는 값 변경 시에만 컴포넌트 리렌더링
이때 주의해야 할 점은 useStoreSelector에 제공하는 두 번째 인수인 selector를 컴포넌트 밖에 선언해야 한다는 것이다. 이것이 불가능하다면 useCallback을 사용해 참조를 고정시켜야 한다. 만약 컴포넌트 내에 이 selector 함수를 생성하고 useCallback으로 감싸두지 않는다면 컴포넌트가 리렌더링될 때마다 함수가 계속 재생성되어 store의 subscribe를 반복적으로 수행할 것이다.
소감
Redux나 Context API, useState를 정해진 문법대로 사용해보기만 하다가 이렇게 store를 직접 코드로 만들어보니까 신기했다. 컴포넌트끼리 일관된 상태를 공유하고 필요한 렌더링만 일어나게 하는 일은 참 어려운 것 같다 ㅎㅎ
'React > 모던 리액트 Deep Dive' 카테고리의 다른 글
[리액트] 1.5 이벤트 루프와 비동기 통신의 이해 (0) | 2024.12.28 |
---|