리액트 훅을 이용한 마이크로 상태 관리
상태 관리는 리액트를 통해 개발할 때 중요한 문제입니다. 리액트 훅이 등장한 이후 상황이 바뀌었습니다. 상태 관리를 위해 훅을 사용할 수 있으며 이를 재사용 가능하고 더 풍부한 기능을 만들기 위한 기반 요소로 사용할 수 있게 되었습니다.
이를 통해 상태 관리를 경량화, 마이크로화 할 수 있었습니다.
전통적인 중앙 집중형 상태 관리는 범용적으로 사용되는 반면 마이크로 상태 관리는 좀 더 목적 지향적이며 특정한 코딩 패턴과 함께 사용됩니다.
이 책에서는 다양한 상태 관리 패턴에 대해 공부하고 전역 상태를 다룹니다.
마이크로 상태 관리 이해하기
리액트에서 상태란 사용자 인터페이스(UI) 에 나오는 모든 데이터를 말합니다. 훅이 나오기 이전에는 중앙 집중형 상태 관리 라이브러리를 사용하는 것이 일반적이었습니다.
중앙 집중형 상태 관리란?
중앙 집중형 상태 관리 라이브러리는 다음과 같은 특징이 있습니다.
중앙화된 저장소(Store): 애플리케이션의 모든 상태를 한 곳에서 관리합니다. 이를 통해 상태의 일관성을 유지하고 쉽고, 상태 변경을 추적하기 쉽습니다.
예측 가능한 상태 변화: 상태가 변경이 될 때마다 특정 규칙(리듀서)에 따라 상태가 변경됩니다. 이를 통해 상태 변화가 예측되고 디버깅이 쉬워집니다.
단방향 데이터 흐름: 액션을 통해 상태 변경이 요청되고 리듀서가 이를 통해 새로운 상태를 생성, 상태가 스토어에 저장되고 컴포넌트는 스토어의 상태를 바라보기에 UI가 새롭게 생성됩니다.
전역 접근: 애플리케이션 내 모든 컴포넌트가 중앙 스토어에 접근할 수 있어, 상태를 관리하기 용이합니다.
(예시) 대표적인 중앙 집중형 상태 관리 라이브러리 Redux, MobX, Recoil, Context API
중앙 집중형 상태 관리 라이브러리는 때로는 가볍게 쓰기에 너무 과한 측면이 없잖아 있었습니다. 때문에 리액트 훅이 등장하면서 새롭게 상태를 생성 및 관리하는 방법이 나오기 시작하였습니다.
범용적인 상태 관리를 위한 방법은 가벼워야 하며, 개발자는 요구사항에 따라 적절한 방법을 할 수 있어야 합니다. 이를 가리켜 마이크로 상태 관리 라고 합니다.
리액트 훅 사용하기
리액트는 컴포넌트 내에서 사용할 수 있는 기본적인 훅을 제공하는데 이를 흔히 지역 상태라고 부릅니다.
const Component = () => {
const [state, setState] = useState(false);
return (
<div>
<button onClick={()=>setState((prev) => !prev)}>
토글
</button>
{show ? "보입니다." : "보이지 않음"}
</div>
)
}
훅의 장점중 하나는 로직의 분리가 가능하다는 점입니다.
const useVisible = () => {
const [visible, setVisible] = useState(false);
const show = () => {
setVisible(true);
}
const close = () => {
setVisible(true);
}
return {visible, show, close}
}
const Component = () => {
const {visible, show, close} = useVisible(false);
return (
<div>
<button onClick={show}>
보이게 한다.
</button>
<button onClick={close}>
안보이게 한다.
</button>
{visible ? "보입니다." : "보이지 않음"}
</div>
)
}
이는 복잡해졌다고 할 수 있지만 여러가지 장점을 가지고 있습니다.
첫 번째로 이름을 지정하므로 코드의 가독성이 높아집니다.
두 번째는 컴포넌트와 코드가 분리됐으므로 컴포넌트의 코드를 수정하지 않고 기능을 추가할 수도 제거할 수도 있습니다.
예를들어 보여질 때마다 로직을 추가하고 싶다면 다음과 같이 추가 할 수 있습니다.
const useVisible = () => {
const [visible, setVisible] = useState(false);
useEffect(()=>{
if(visible){
console.log("show 버튼을 눌렀습니다.")
}
},[visible])
const show = () => {
setVisible(true);
}
const close = () => {
setVisible(false);
}
return {visible, show, close}
}
이처럼 useVisible
의 로직만 변경해서 디버깅 로직을 추가할 수 있습니다.
전역 상태 탐구하기
전역 상태는 애플리케이션 내 서로 멀리 떨어져있는 여러 컴포넌트에서 사용하는 상태를 말합니다.
const Component = () => {
const { visible, show } = useGlobalVisible();
return (
<div>
<button onClick={close}>닫기</button>
{visible && <Modal />}
</div>
)
}
const Modal = () => {
const { close } = useGlobalVisible();
return (
<div>
<button onClick={close}>닫기</button>
모달이 열려있습니다.
</div>
)
}
위 예시에서 Component와 Modal은 서로 같은 useGlobalVisible을 공유하고 있습니다. 이를 눌렀을 때 모달의 상태를 공유하는 것을 기대할 수 있습니다.
이는 Props를 이용하여 구현을 할 수 있지만, 전달하지 않고 동일한 상태를 공유하는 것은 어렵습니다. 리액트에서 제공하지 않기 때문에 이를 구현하는 것은 개발자나 커뮤니티의 몫입니다.
useState 사용하기
지연 초기화
useState는 첫 번째 렌더링에서만 평가되는 초기화 함수를 받을 수 있습니다.
const init = () => 1;
// 지연 초기화 함수의 예시
const Component = () => {
const [count, setCount] = useState(init);
return (
<div>
{count}
</div>
)
}
1을 가져오는 것은 성능에 영향을 주는 것이 아니기에 예제로서는 적합하지 않습니다. 하지만 init 안에 들어가는 함수에는 무거운 계산을 하는 함수가 있다고 하였을 때 이는 초기 상태를 가져올 때만 호출을 합니다. 즉 컴포넌트가 마운트 될 때 한 번만 호출이 됩니다.
베일 아웃
베일 아웃을 통해 렌더링을 방지할 수 있습니다.
const Component = () => {
const [count, setCount] = useState(0);
const onClick = () => {
setCount((c) => c % 2 === 0 ? c : c + 1); // 홀수일 때만 상태 변경
}
return (
<div>
<button onClick={onClick}>
count가 홀수일 때만 렌더링이 됩니다.
</button>
{count}
</div>
)
}
만약 똑같은 값이 상태 변경 값으로 주어질 경우 렌더링이 되지 않습니다.
useReducer 사용하기
리듀서는 복잡한 상태에 유용합니다.
const reducer = (state, action) => {
switch(action.type) {
case 'INCREMENT' :
return {...state, count: state.count + 1};
case 'DECREMENT' :
return {...state, count: state.count - 1};
case 'SET_TEXT' :
if(!action.text.trim()){
return state
}
return {...state, text: action.text};
default :
throw new Error('unknown action type');
}
}
useState는 객체가 아닌 숫자나 문자열 같은 원시 값에 대해 작동합니다. 원시 값을 사용하는 것은 외부에서 복잡한 리듀서 로직을 정의할 수 있기 때문에 유용합니다.
useState와 useReducer의 유사점과 차이점
useReducer를 이용한 useState 구현
const useState = (initialState) => {
const [state, dispatch] = useReducer(
(prev, action) =>
typeof action === 'function' ? action(prev): action);
);
return [state, dispatch];
}
다음과 같이 단순화 할 수 있습니다.
const reducer = (prev, action) =>
typeof action === 'function' ? action(prev) : action;
const useState = (initialState) =>
useReducer(reducer, initialState);
반대의 경우
const useReducer = (reducer, initialState) => {
const [state, setState] = useState(initialState);
const dispatch = (action) =>
setState(prev => reducer(prev, action));
return [state, dispatch];
}
정리
리액트 훅에서 중요한 역할을 하는 마이크로 상태 관리를 정의하였으며, useState와 useReducer의 차이가 별로 없음을 알게 되었고 상태 값을 통해 할 수 있는 것들이 무언지 알게되었습니다. 다음 장에서는 전역 상태에 대해 알아봅니다.