React 스터디 4주차 global state - useContext

Yunes·2023년 8월 27일
0

리액트스터디

목록 보기
9/18
post-thumbnail

글로벌 state 관리는 왜 필요할까?

리액트에서의 context 가 무엇인지 아는가?

일반적으로 리액트에서 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때 props 를 통해 정보를 전달한다. 그런데 만약 중간에 많은 컴포넌트가 있거나 같은 정보를 필요로 하는 컴포넌트가 많을 때 props 를 전달하는 것은 복잡해질 수 있다.

가장 가까운 공통 조상은 해당 데이터를 필요로 하는 컴포넌트들로부터 멀리 떨어질 수 있고 lifting state up 은 해당 데이터를 얻기 위해 컴포넌트가 계층 구조를 거치며 prop drilling 이라고 불리는 번거로운 상황을 초래할 수 있다.

props 를 순서대로 전달할때글로벌 state 를 사용할 때

이미지 출처 : 모던 자바스크립트로 배우는 리액트 입문

버킷 릴레이

  • 불필요하게 props 를 전달하는 것을 의미한다.
  • 버킷 릴레이 방식은 중간에 props 를 전달하는 컴포넌트들도 props 로 아무것도 하지 않고 전달만 할지라도 props 가 변경되면 재렌더링된다.
lifting state upprop drilling

lifting state up

  • 리액트에서 두 개 이상의 컴포넌트가 상태 변화를 동시에 처리하고 싶을때 state 를 각 컴포넌트에서 제거하고 가장 가까운 공통 부모 컴포넌트로 state 를 옮긴 후 props 를 통해 그들에게 내려 전달하는 방식을 말한다.

prop drilling

  • 리액트 애플리케이션에서 데이터를 전달하는 과정에서 발생할 수 있는 문제를 가리키는 용어다.

context 는 부모 컴포넌트가 정보를 전달하고자 할때 그 대상이 어떤 컴포넌트이든지, 얼마나 깊든지 간에 (중간에 컴포넌트가 많다는 의미) props 로 전달할 필요 없이 정보를 마치 텔레포트하듯이 전달할 수 있게 한다.

context : props 전달 방식의 대안

컨텍스트는 부모컴포넌트에게 트리의 자식 컴포넌트로 데이터를 전달하는 방법을 제공한다.

코드 출처 : react.dev

현재 코드를 보면 Heading 이 어떤 헤딩 태그를 가질지 h1/h2/.../h6 결정하기 위해 모든 Heading 에 props 로 level 을 전달하고 있다.

그러는 대신 Heading 에서 props 를 제거하고 Section 에서 level prop 을 전달하는 것이 더 좋아보인다.

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

그럼 이제 Heading 컴포넌트는 가장 가까운 Section 컴포넌트로부터 어떻게 level 에 대해 알 수 있을까? Heading 같은 자식 컴포넌트들은 트리에서 데이터를 갖는 컴포넌트에게 데이터에 대해 물어볼 방법이 필요하다.

이때 context 가 사용된다.

이미지 출처: react.dev passing data deeply with context

codesandbox 코드는 오른쪽의 바를 왼쪽으로 드래그하면 실행화면을 볼 수 있다.

context 를 사용하는 방법

  1. Create - context생성 한다.

  2. Use - 데이터를 필요로하는 컴포넌트에서 해당 context사용 한다.

  3. Provide - 데이터를 갖는 컴포넌트는 context제공 한다.

Create context

react 는 createContext 를 제공하여 context 를 생성할 수 있도록 한다. createContext 로 만든 Context 는 해당 파일로부터 export 해줘야 다른 컴포넌트에서 이 context 를 사용할 수 있다.

코드 출처 : react.dev - passing data deeply with context

createContext 가 갖는 유일한 인자는 기본값이다. 이 인자로 object 를 포함하여 어느 종류의 값도 전달할 수 있다.

Use context

데이터를 필요로하는 컴포넌트에서 useContext 훅과 앞에서 생성한 context 를 import 한다.

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

위의 예시에서 Heading 컴포넌트가 level 이라는 data 를 사용하고자 하는 컴포넌트에 해당한다.

앞에서 Heading 컴포넌트는 level 을 props 로 전달받고 있었으나 이를 제거하고 useContext에 미리 생성한 context 를 인자로 전달한다.

// 기존 Heading 컴포넌트
export default function Heading({ level, children }) {
	// ...
}

// 수정한 Heading 컴포넌트
export default function Heading({ children }) {
  	const level = useContext(LevelContext);
	// ...
}

useContext 훅은 React 에게 Heading 컴포넌트가 LevelContext 라는 context 를 읽고 싶어한다고 말해준다.

useContext 를 사용한 뒤의 컴포넌트

context 를 생성하고 사용하고자 했다. 그러나 아직 context 를 사용할 수 없다. 그러기 전에 context 를 먼저 provide 해줘야 한다. 그렇지 않으면 LevelContext 에서 기본값으로 넣은 1 이 모든 Headinglevel 이 된다.

Provide context

위의 예시에서 Section 컴포넌트는 children 을 렌더링하고 있다.

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}

이 컴포넌트를 context provider 로 감싸서 LevelContextprovide 할 수 있도록 하자.

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

이는 React 에게 만약 Section 컴포넌트 내에 LevelContext 에 대해 물어보는 컴포넌트가 있다면 그 컴포넌트에게 level 을 전달하라고 알려준다.

전체 흐름 :
1. Section 에게 level prop 전달한다.
2. Section <LevelContext.Provider value={level}>children 을 감싼다.
3. HeadinguseContext(LevelContext) 를 갖고 LevelContext 의 value 에 대해 물어본다.

react 공식문서 에서 create - use - provide 순서로 useContext 를 설명하는데 개인적으로는 흐름상 create - provide - use 순서로 설명하는게 더 자연스러워 보였다.

최종 형태

context 를 사용하기 전에 알아야 할 것

context 가 사용하기 쉬워서 과하게 사용하게 될 수 있다. 어떤 props 을 몇 단계 깊이로 전달한다는게 반드시 이 정보를 context 에 넣어야 한다는 것을 의미하지는 않는다.

passing props 로 시작하자 :
전달하는 prop 이 많지 않을때가 더 흔한 상황이다. 때로는 prop 로 전달하는게 어떤 데이터를 사용하는 지 분명하게 보이기도 한다.

component 를 추출하고 JSX 를 children 으로 전달하자 :
<Layout posts={posts} /> 와 같이 post라는 데이터를 직접 사용하지 않는 경우 Layout 이 children 을 prop 으로 받아 <Layout><Posts posts={posts} /></Layout> 로 처리하면 불필요한 layer 를 줄일 수 있다.

context 를 사용하는 경우

  • 테마 설정 : 사용자가 앱의 외관을 변경할 수 있는 경우 앱의 맨 위에 context provider 를 배치하고 시각적인 모양을 조정해야 하는 경우 컴포넌트에서 context 를 사용할 수 있다.

  • 현재 계정 : 많은 컴포넌트가 현재 로그인한 유저들에 대해 알아야 할 때가 있을 수 있다. 이걸 컨텍스트에 넣으면 트리의 어디에서나 편하게 읽을 수 있다.

  • 라우팅 : 대부분의 라우팅 솔루션은 현재 경로를 보유하기 위해 내부적으로 context 를 사용한다.

  • 상태 관리 : 앱의 규모가 커지며 앱의 상단에 많은 상태가 몰리게 될 것이다. 하위의 많은 먼 컴포넌트에서도 이를 변경하고 싶어할 수 있다. 보통 복잡한 상태를 관리하고 하위의 먼 컴포넌트로 전달하기 위해 reducer 를 context 와 함께 사용한다.

Context 객체 하나의 값이 바뀌었을 때 useContextContext 를 참조하고 있는 컴포넌트는 모두 재렌더링된다.
따라서 하나의 context 에 다양한 state 를 함께 두는 것은 피해야 한다.

그럼 이제 useContext 가 무엇인지 알아보자

useContext 훅은 컴포넌트에서 context 를 읽고 subscribe 할 수 있도록 한다.

const value = useContext(SomeContext)
  • someContext : 미리 createContext 로 생성한 context. 일반적으로는 context 자체만으로는 정보를 갖고 있지 않고 컴포넌트로부터 provide 혹은 읽을 정보의 종류를 나타낸다.

context 를 읽고 subscribe 하기 위해 useContext 를 컴포넌트의 상단에서 호출하자.

import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...
  • useContextcontext value 를 반환한다. 이 value 는 가장 가까운 SomeContext.Provider 로부터 전달받아 결정된다.
  • Provider 는 가장 가까운 상위 컴포넌트에서 찾고 useContext 를 호출하고 있는 컴포넌트의 provider 는 고려하지 않는다.
  • 만약 context 가 바뀌면 이 context 를 읽는 컴포넌트들은 재렌더링된다.
  • 만약 상위 컴포넌트에서 Provider 를 찾지 못하면 useContext() 로 반환하는 context valuecreateContext 로 전달한 default value 가 된다.

object, function 을 전달하는 경우 재렌더링을 최적화하는 방법

context value 로 object, function 을 포함한 어느 값도 전달할 수 있다.

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  function login(response) {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }

  return (
    <AuthContext.Provider value={{ currentUser, login }}>
      <Page />
    </AuthContext.Provider>
  );
}

function 의 경우 리액트가 매번 재렌더링 할 때마다 새로운 함수를 생성하므로 참조하는 값이 달라진다. 그러면 useContext 를 호출하고 있는 모든 컴포넌트는 재렌더링이 된다.

이때 currentUser 의 경우엔 변하지도 않았는데 재렌더링을 할 필요가 없다.

이 상황에서 리액트는 재렌더링을 최적화하기 위해 useCallback 으로 login 함수를 감싸고 contextValue 를 useMemo 로 감싸서 코드를 개선할 수 있다.

import { useCallback, useMemo } from 'react';

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = useMemo(() => ({
    currentUser,
    login
  }), [currentUser, login]);

  return (
    <AuthContext.Provider value={contextValue}>
      <Page />
    </AuthContext.Provider>
  );
}

이전 포스트에서 useCallback, useMemo 에 대해 정리한 글을 다시 참고했다.

  • useCallback : 함수 메모이제이션. 재렌더링 사이에 함수 정의를 캐시할 수 있게 해준다. 위의 코드에선 로그인은 첫 렌더링 이후 바뀌지 않을 것을 상정하여 dependencies 에 빈 배열을 넣었다.
  • useMemo : 변수 메모이제이션. 재렌더링 사이에 계산의 결과를 캐시해준다.

위의 코드의 경우 currentUser 가 바뀌지 않는 한 useContext(AuthContext) 는 재렌더링하지 않는다.

TODO

useReducer 에 대한 학습을 진행한 후 React 가 reducer 와 context 를 사용하여 어떻게 상태관리를 하고 있는지 정리하자.

레퍼런스

book
모던 자바스크립트로 배우는 리액트 입문
docs
react.dev - context 로 데이터를 깊이 전달

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글