02. 지역 상태와 전역 상태 이용하기
다이시 카토의 리액트 훅을 활용한 마이크로 상태 관리 2부
2024-06-06
리액트 컴포넌트는 트리 구조를 구성합니다. 트리 구조에서 하위 트리 내에 상태를 만드는 것은 간단한데, 단순히 트리의 상위 컴포넌트에서 지역 상태를 만들고 컴포넌트와 자식 컴포넌트에서 해당 상태를 사용하기만 하면 됩니다. 이런 방법은 지역성과 재사용성 측면에서 좋기 때문에 보통 이런 전략을 따르는 것이 권장됩니다.
언제 지역 상태를 사용할까?
특정 상황에서는 트리 내 서로 멀리 떨어져 있는 둘 이상의 컴포넌트에 공통적인 상태가 필요한 경우가 있습니다. 이런 경우 전역 상태가 필요합니다. 전역 상태는 지역 상태와는 다르게 개념적으로 특정 컴포넌트에 속해 있지 않으므로 전역 상태를 저장하는 위치를 고려해야 합니다.
함수와 인수
다음은 같은 이수에 대해 항상 같은 값을 반환하는 순수 함수입니다.
const addOne = (n) => n + 1;
순수 함수는 동작을 예측할 수 있기 때문에 선호되는 경우가 많습니다.
다음은 전역 변수에 의존하는 함수입니다. base의 값이 변경되면 함수가 다르게 동작합니다.
let base = 1;
const addBase = (n) => n + base;
전역 변수에 의존하는 함수를 무조건 나쁘다고 볼 수는 없지만, 의도하지 않은 결과가 나올 수 있다는 위험성이 항상 존재합니다. 또한 전역 변수인 base가 싱글턴이기 때문에 재사용성이 떨어집니다.
다음은 컨테이너 객체를 통해 재사용성을 높인 접근 방식입니다.
const createContainer = () => {
let base = 1;
const addBase = (n) => n + base;
const changeBase = (b) => { base = b };
return { addBase, changeBase };
};
const { addBase, changeBase } = createContainer;
컨테이너 객체마다의 base는 격리돼 있으므로 서로 다른 컨테이너에 영향을 주지 않습니다. 따라서 재사용하기가 쉬워집니다.
리액트 컴포넌트와 props
리액트는 개념적으로 상태를 사용자 인터페이스(UI)로 변환하는 함수다. 리액트로 코드를 작성할 때 리액트 컴포넌트는 말 그대로 자바스크립트 함수이며 그것의 인수를 props라고 한다.
리액트 컴포넌트의 개념에 대해 설명한 한 줄의 문장이 인상 깊었습니다. 리액트 컴포넌트도 결국 props라는 이름의 인자를 받아 동작하는 자바스크립트 함수이며, 그 함수의 역할은 상태를 UI로 변환하는 것입니다.
리액트 컴포넌트 내에 지역 상태가 존재하지 않는다면 리액트 컴포넌트는 인자를 받으면 항상 같은 결과를 내뱉는 순수 함수입니다. 하지만 컴포넌트 내에 상태가 존재하고 그것을 사용한다면 순수하지 못한 함수가 됩니다. 그러나 상태가 컴포넌트 내에서만 사용된다면 다른 컴포넌트에 영향을 미치지 않고, 책에서는 이러한 특성을 **‘억제됨(contained)’**이라고 표현합니다.
지역 상태에 대한 useState 이해하기
useState가 포함된 AddBase 함수는 changeBase를 함수 선언 범위 내에서만 사용할 수 있기 때문에 억제됐다고 할 수 있다. 함수 밖에서 base를 변경하는 것은 불가능하다. useState를 이렇게 사용하는 것은 지역 상태를 사용하는 것에 해당하며, 컴포넌트는 억제돼 있고 컴포넌트 외부의 그 어떤 것에도 영향을 미치지 않기 때문에 지역성을 보장한다.
const AddBase = ({ number }) => {
const [base, changeBase] = useState(1);
return <div>{number + base}</div>;
};
useState는 state와 setState를 튜플로 반환합니다. useState가 변경되지 않는 한 state를 반환하므로 useState를 사용하는 리액트 컴포넌트 함수는 멱등성을 지닙니다.
또한 useState가 포함된 AddBase 함수는 changeBase를 함수 선언 범위 내에서만 사용할 수 있기 때문에 억제됐다고 할 수 있습니다. 컴포넌트가 억제돼있다는 것은 컴포넌트 외부의 그 어떤 것에도 영향을 미치지 않는다는 뜻이므로 지역성을 보장합니다.
지역 상태의 한계
상태 변수는 개념적으로 전역 변수다. 전역 변수는 함수 외부에서 자바스크립트 함수의 동작을 제어할 때 유용하게 사용할 수 있다. 마찬가지로 전역 상태는 컴포넌트 외부에서 리액트 컴포넌트의 동작을 제어할 때 유용하게 사용할 수 있지만 컴포넌트 동작을 예측하기 어렵다는 장단점이 있다. 따라서 전역 상태를 과하게 사용해서는 안 된다. 지역 상태를 기본으로 사용하고, 전역 상태는 보조 수단으로 사용하는 것이 좋다.
지역 상태는 당연하게도 지역성을 제공하고 싶지 않을 때는 적절하지 않습니다. 위 AddBase 컴포넌트의 예시를 이어가자면, 완전히 다른 코드 영역에서 base를 변경하고 싶다면 지역 상태를 사용하는 것이 적절하지 않죠. 이렇게 함수 컴포넌트 외부에서 상태를 변경해야 할 때 전역 상태가 필요합니다.
지역 상태를 효과적으로 사용하는 방법
상태 끌어올리기
const Component1 = ({ count, setCount }) => }
return (
<div>
{count}
<button onClick={() => setCount((c) => c + 1)}>
Increment Count
</button>
</div>
);
};
const Component1 = ({ count, setCount }) => }
return (
<div>
{count}
<button onClick={() => setCount((c) => c + 1)}>
Increment Count
</button>
</div>
);
};
const Parent = () => {
const [count, setCount] = useState(0);
return (
<>
<Component1 count={count} setCount={setCount} />
<Component2 count={count} setCount={setCount} />
</>
);
};
이렇게 상태를 Parent에서 한 번만 정의하고 자식 컴포넌트에 공유하는 패턴을 책에선 상태 끌어올리기라고 표현했습니다.
이 패턴은 지역 상태를 사용하는 대부분의 상황에서 작동하지만 성능 문제가 있을 수 있습니다. 상태를 상위 컴포넌트로 전달할 경우 Parent는 모든 자식 컴포넌트를 포함해 하위 트리 전체를 리렌더링할 것이기 때문입니다.
내용 끌어올리기
const AdditionalInfo = () => {
return <p>Some information</p>
};
const Component1 = ({ count, setCount, children }) => }
return (
<div>
{count}
<button onClick={() => setCount((c) => c + 1)}>
Increment Count
</button>
{children}
</div>
);
};
const Parent = ({ children }) => }
const [count, setCount] = useState(0);
return (
<>
<Component1 count={count} setCount={setCount}>
{children}
</Component1>
<Component2 count={count} setCount={setCount} />
</>
);
};
const GrandParent = () => {
return (
<Parent>
<AdditionalInfo />
</Parent>
);
};
복잡한 컴포넌트에서는 상위 컴포넌트의 상태에 의존하지 않는 컴포넌트가 당연하게도 있을 수 있습니다. 그 상태에 의존하지 않는데도 자식 컴포넌트라는 이유만으로 상태가 변경됐을 때 리렌더링된다면 너무 억울하겠죠?
이러한 억울함을 방지하기 위해서 JSX 요소를 상위 컴포넌트로 끌어올릴 수 있습니다. 또한 코드 예시처럼 children prop을 이용해서 내용 끌어올리기 패턴을 구현할 수 있습니다.
전역 상태 사용하기
전역 상태란?
이 책에서 전역 상태는 단순히 지역 상태가 아님을 의미한다. 개념적으로 하나의 컴포넌트에 속하고 컴포넌트에 의해 캡슐화된 상태를 지역 상태라고 한다. 따라서 상태가 하나의 컴포넌트에만 속하지 않고 여러 컴포넌트에서 사용할 수 있다면 전역 상태라고 한다.
언제 전역 상태를 사용할까?
전역 상태는 주로 지역 상태 대신 사용되며, 전역 상태를 사용하기 좋은 두 가지 패턴이 있다. 하나는 prop 전달이 적절하지 않은 경우고 다른 하나는 전역 상태가 이미 애플리케이션에 존재하는 경우다.
우리가 보통 언제 전역 상태의 필요성을 느낄까 생각해보면 컴포넌트 간에 상태를 공유해야하는데 그 거리가 너무 멀어서 불필요하게 상태를 깊게 전달해야할 때가 가장 먼저 떠오릅니다. 중간 컴포넌트를 두어서 그 컴포넌트가 상태를 전달해줄 수 있지만 이는 너무 번거로운 작업이고 당연히 개발자 경험도 좋지 못합니다. 또한 상태가 변경되면 그 상태를 직접적으로 사용하지 않는 중간 컴포넌트도 리렌더링이 발생하며 이는 성능에도 영향을 미칠 수 있습니다.
이미 리액트 외부에 상태가 있을 때에도 전역 상태 사용을 고려할 수 있습니다. 책에서는 이 케이스에 대해 다음 예시를 사용합니다.
const globalState = {
authInfo: { name: 'React' },
};
const Component = () => {
// useGlobalState는 가상의 훅이다
const { authInfo } = useGlobalState();
return (
<div>
{authInfo.name}
</div>
);
};
정리
이번 장에서는 지역 상태의 대한 개념과 지역 상태를 효과적으로 사용하는 몇 가지 패턴을 살펴본 후 지역 상태의 한계와 전역 상태를 사용할 만한 케이스에 대해 학습할 수 있었습니다.
리액트 컴포넌트의 개념을 인자를 받아서 사용자 인터페이스를 리턴하는 함수로 본 것도 인상 깊었고, 자주 사용하던 지역 상태를 다시 한번 생각할 수 있는 기회가 되었습니다.
또한 “전역 상태를 남용하면 안되겠다”라는 생각을 다시 한 번 되새길 수 있는 기회가 됐던 것 같습니다.