React | React.Context 를 활용한 전역 상태 관리

imzzuu·2022년 5월 30일
0

전역 상태 관리란?


특정 컴포넌트에서의 state 변화를 전역상태에 저장해두고,필요한 컴포넌트에서 사용 하는 것이다.

전역상태관리를 사용하지 않는 경우, 특정 컴포넌트에서의 state 변화를 다른 컴포넌트에게 전달하기 위해 여러차례 props를 전달해야함 (이를 props drilling 이라고 부름)

이와 같은 경우를 보완하고자 state 변화를 store 에 저장해두고, 전역에서 사용할 수 있도록 하는 것이다.
이 때, store는 여러개가 될 수도 있다.

단점

  • 컴포넌트 재사용이 어려움
  • 컴포넌트 유닛테스트 작성이 어려움(함수 밖 state에 영향을 받기 때문에 mocking 해주어야함)

공식문서에서는 단순하게 props drilling 해결을 위해서 전역상태관리를 활용하기보단,
합성 컴포넌트를 사용하는 것이 더 간단한 해결책이라고 한다.

props drilling을 해결하기 위해 컴포넌트 합성을 이용한 예시

const Hello1 = (props) => (
  <div>
this is Hello1.
<Hello2 name={props.name} /> 
  </div>
);
const Hello2 = (props) => (
  <div>
this is Hello2.
<Hello3 name={props.name} />
 </div>
);
const Hello3 = (props) => (
  <div>
this is Hello3.
<Hello4 name={props.name} />
 </div>
);
const Hello4 = (props) => (
  <div>
this is Hello4.
<div>Hello {props.name}!</div>
 </div>
);

// App.tsx
<Hello1 name='asdf' />

최상위 App.jsx 에서 <Hello1/> 만 작성하고, prop 을 넘겨준 후, 하위 컴포넌트를 통해서 prop 을 계속 넘겨주는 코드이다.

이 경우, 단지 <Hello4/> 에서 사용하려고 props drilling 를 통해 props.name을 출력하게 된다.

컴포넌트 합성 활용

const Hello1 = (props) => (
  <div>
this is Hello1.
{props.children} </div>
);
const Hello2 = (props) => (
  <div>
this is Hello2.
{props.children} </div>
);
const Hello3 = (props) => (
  <div>
this is Hello3.
{props.children} </div>
);
const Hello4 = (props) => (
  <div>
this is Hello4.
<div>Hello {props.name}!</div> </div>
);

// App.tsx
<Hello1>
  <Hello2>
		<Hello3>
			<Hello4 name={props.name} >
    </Hello3>
  </Hello2>
</Hello1>

최상위 App.jsx 에서 모든 컴포넌트를 중첩하여 합성 컴포넌트 형태로 작성한다면,
나머지 컴포넌트에서는 props.children 으로 랜더링을 해주고, <Hello4/> 으로만 name 를 전달해줄 수 있다.

즉, name의 value를 알아야하는건 App.tsx 뿐이다 (다른 component 들은 name 을 전달받지 않음)

하지만, 이렇게 간단한 로직이 아닌 복잡한 로직을 상위에 작성한다면, 오히려 더 난해해질 수 있으니 각자의 로직에 알맞는 방법을 적용해야할 것이다.

그렇다면, 전역 상태 관리는 어떨 때 사용하면 좋은가?

단순 props drilling 이슈보다, 모든 컴포넌트에서 props 를 사용해야할 경우에 적용하면 좋다.

아래의 코드는 전역으로 let name = ''; name 을 선언하고,
각 컴포넌트에서 name 을 랜더링해주는 코드이다.

let name = '';

const Hello1 = () => (
  <div>
this is Hello1. and Name is {name}
    <Hello2 />
  </div>
);
const Hello2 = () => (
  <div>
this is Hello2. and Name is {name}
    <Hello3 />
  </div>
);
const Hello3 = () => (
  <div>
this is Hello3. and Name is {name}
    <Hello4 />
  </div>
);
const Hello4 = () => (
  <div>
this is Hello4.
<div>Hello {name}!</div>
 </div>
);

// App.tsx
name = 'asdf';
<Hello1 />

이 코드의 문제점
⇒ name 변수의 value를 바뀌어도컴포넌트가 re-render되지 않는다.

컴포넌트의 re-render 조건은

  1. props 의 변경
  2. state 의 변경
  3. 부모 컴포넌트의re-render

이다.

App.tsx 에서 name = 'asdf' 이라고 할당을 했고, 그 값을 바꾼다고 해도 re-render 가 일어나지 않을 것이다.

위의 코드는

  1. props 를 통해 값을 전달하지 않았다.
  2. state를 통해 값을 관리하지 않았다. (useState 의 setter 함수 사용해서 값을 바꾸지 않음)
  3. 그렇기 때문에 당연히 부모 컴포넌트에서도 re-render 가 일어나지 않는다.

때문에 이런 방식의 상태 전역 관리보다 React context API 를 통해 전역 상태 관리를 진행한다.

React context


  • createContext를 통해 전역 상태 선언
  • Context.Provider 안쪽에 render된 컴포넌트들은
  • useContext hook을 통해 context의 value에 접근 가능

사용법

import { createContext, useContext } from "react"; 

const defaultName = 'asdf';
// context 만들기 (value와 자료형 맞춰주기)
const NameContext = createContext(defaultName);
// => NameContext의 자료형 = 객체

const Hello1 = () => {
// useContext를 사용해서 State 값 접근
const name = useContext(NameContext); 
return (
<div>
this is Hello1. and Name is {name} <Hello4 />
</div> );
};

const Hello4 = () => {
// useContext를 사용해서 State 값 접근
const name = useContext(NameContext); 
return (
<div>
this is Hello4 <div>Hello {name}!</div>
</div> );
};

const App = () => (
// Provider 로 감싸주어서 사용
<NameContext.Provider value={defaultName} >
    <Hello1 />
  </NameContext.Provider>
);
export default App;

실제로 context 로 관리하는 state가 변경 되었을 때, re-render 가 되는지 예제로 알아보자!

예제

import { createContext, useContext, useState, useMemo } from "react";

// context 생성
const NameContext = createContext("");

const Hello1 = () => {

  // useContext 사용하여 값 접근
  const name = useContext(NameContext);

  // useMemo를 사용하여, 최초 렌더링 일 때만, <Hello4 /> 컴포넌트 그리기
  const hello4 = useMemo(() => {
    console.log("re-render in hello1"); // => 실제로 최초 랜더링에만 console 찍힘
    return <Hello4 />;
  }, []);

  return (
    <div>
      this is Hello1. and Name is {name} {hello4}
    </div>
  );
};

const Hello4 = () => {
  // useContext 사용하여 값 접근
  const name = useContext(NameContext);
  console.log("re-render in hello4"); // => 전역으로 관리하는 state (name) 이 변경될 때마다 re-rendering

  return (
    <div>
      this is Hello4 <div>Hello {name}!</div>
    </div>
  );
};

const ContextEx = () => {
  // input 의 값을 state 로 관리하여, 그 값을 NameContext 의 value 로 사용
  const [name, setName] = useState();

  return (
    <>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <NameContext.Provider value={name}>
        <Hello1 />
      </NameContext.Provider>
    </>
  );
};
export default ContextEx;

부모 컴포넌트 Hello1 에서 useMemo 를 통해 최초 한번만 랜더링 하도록 처리했어도,

Hello4 에서 useContext 를 통해 관리 되는 값을 사용하고 있기 때문에, re-rendering 이 일어난다.

이제 React context 배운 이후로 React 에서 re-render 되는 조건에는 한가지가 추가 되었다.

  1. props 의 변경
  2. state 의 변경
  3. 부모 컴포넌트의 re-render
  4. context-consumer (useContext) (상위의 3가지 조건에 부합하지 않아도 re-render 된다)
profile
FrontenDREAMER

0개의 댓글