Context API 뿌시기

우디(박연기)·2025년 5월 25일

자꾸 무언갈 부시는 호이초이 블로그를 보고 조심스레 제목을 베껴보았습니다 ㅎㅎ..

Context란 무엇인가?

Context는 부모 컴포넌트가 트리 아래에 있는 모든 컴포넌트에 깊이에 상관없이 정보를 명시적으로 props를 통해 전달하지 않고도 사용할 수 있게 해줍니다.

공식문서에서 Context를 위와 같이 설명하고 있다.

쉽게 말해서 Context는 리액트 컴포넌트 트리 안에서 “전역적으로 값을 공유” 할 수 있게 해주는 기능이다.

즉, “부모 → 자식 → 손자 → 증손자” 이런 식으로 일일이 props로 넘기지 않고, “부모 → 손자”로 필요한 컴포넌트가 바로 값을 가져다 쓸 수 있게 하는 것이다.

어떻게 사용할까?

  1. Context 만들기

    • createContext() 라는 리액트에서 제공하는 함수를 이용하여 Context를 만든다.
      import { createContext } from 'react';
      
      export const ThemeContext = createContext('light');
      • 첫번째 인자로 초기값을 넣어준다. 해당 초기값은 나중에 useContext로 값을 찾을 때 부모에 값이 존재하지 않는 경우 초기값이 사용된다.
  2. Provider로 값을 감싼다

    • 공유할 값을 Context.Provider를 통해 하위 컴포넌트에 전달한다.
      import { ThemeContext } from './ThemeContext';
      
      export default function App() {
        const theme = 'dark'; 
      
        return (
          <ThemeContext.**Provider** value={theme}>
            <Page />
          </ThemeContext.**Provider**>
        );
      }
  3. 값이 필요한 곳에서 useContext로 값을 꺼내 사용한다.

    import { useContext } from 'react';
    import { ThemeContext } from './ThemeContext';
    
    export default function Page() {
      const theme = useContext(ThemeContext);
    
      return <div>현재 테마: {theme}</div>;
    }

 useConext()를 호출하는 컴포넌트는 그 위의 가장 가까운 컨텍스트의 value를 받게 된다.

항상 Context를 사용해야 할까?

먼가 위에 내용만 보면 Context를 사용하면 귀찮게 props를 뚫지 않아도 되니까 항상 Context를 사용하는 것이 좋아보인다. 하지만 실제로는 그렇지 않다. Context는 정말 필요할 때만 사용해야 한다.

그렇다면 무분별한 Context 사용의 단점을 알아보자.

  1. 모든 컴포넌트가 강하게 엮이게 된다
    • props 대신에 Context가 제공하는 값에 의존하게 되면 해당 컴포너트가 Context에 강하게 엮이게 된다.
    • 이렇게 되면 컴포넌트의 재사용성이 떨어지고 Context 없이 사용할 수 없는 컴포넌트가 된다.
  2. Context 값이 바뀌면 모든 구독 컴포넌트가 재렌더링된다
    • Context를 구독하는 모든 컴포넌트는, Provider의 value가 변경될 때마다 무조건 리렌더링된다.
    • 만약 전체 App이나 많은 컴포넌트가 구독하고 있다면, 작은 값 하나 변경으로 불필요한 리렌더링이 발생해 성능 문제가 생길 수 있다.
  3. 상태 관리가 더 복잡해질 수 있다
    • props를 통해 명시적으로 내려주는 것보다, Context를 많이 쓰면 데이터 흐름이 눈에 안 보이게 돼서 디버깅이 어려진다.
    • 컴포넌트가 왜 이렇게 동작하지를 알기 위해 전부 따라가야 한다.

이런 단점 때문에 무분별하게 Context를 사용하면 안된다.

언제 사용해야 할까?

  1. 여러 컴포넌트가 같은 값을 공유하는 경우

    • ex) CardPreview 컴포넌트와 CardInfoForm 컴포넌트에서 같은 카드 정보 상태를 공유하는 경우
      function App() {
        const { cardNumber, expirationPeriod, CVCNumber, password, cardType } =
          useAllCardInfo();
      
        return (
          <StyledApp>
            <StyledFrame>
              <CardPreview
                **// cardNumber={cardNumber.values}
                // expirationPeriod={expirationPeriod.values}
                // cardType={cardType.values.cardType}**
              />
              <CardInfoForm
                // **cardNumber={cardNumber}
                // expirationPeriod={expirationPeriod}
                // cardType={cardType}**
                CVCNumber={CVCNumber}
                password={password}
              />
            </StyledFrame>
          </StyledApp>
        );
      }
  2. props drilling(깊게 props를 계속 넘겨야 하는 문제)을 해결하고 싶은 경우

    어떤 prop을 자식 컴포넌트를 통해 깊이 전해줘야 하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우에 복잡하고 불편할 수 있다. 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상은 트리 상 높이 위치할 수 있고 “Prop drilling”이라는 상황을 초래할 수 있다.

    image.png

    ex) CardInfoFormCard~~SectionCard~~Inputs 로 props를 넘기는 경우

     <CardInfoForm
         **// cardNumber={cardNumber}**
         expirationPeriod={expirationPeriod}
         CVCNumber={CVCNumber}
         password={password}
         cardType={cardType}
     />
     
     <CardNumberSection
         **// cardNumber={cardNumber}**
         cardNumberError={cardNumberError}
     />
     
     <CardNumberInputs
    	   **// cardNumber={cardNumber}**
         cardNumberError={cardNumberError}
     />

하지만 공식문서에서 props drilling이 발생한다고 Context를 무조건적으로 사용하는 것을 우려하고 있다.

아래 props drilling + 아래 2가지를 만족하지 않을 때 context를 사용하는 것을 권장?하고 있다.

  1. Props 전달하기로 시작하기. 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 아닙니다. 힘든 일처럼 느껴질 수 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확히 해줍니다. 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋습니다.
  2. 컴포넌트를 추출하고 JSX를 children으로 전달하기. 데이터를 사용하지 않는 많은 중간 컴포넌트 층을 통해 어떤 데이터를 전달하는 (더 아래로 보내기만 하는) 경우에는 컴포넌트를 추출하는 것을 잊은 경우가 많습니다. 예를 들어 posts처럼 직접 사용하지 않는 props를 <Layout posts={posts} />와 같이 전달할 수 있습니다. 대신 Layout은 children을 prop으로 받고 <Layout><Posts posts={posts} /><Layout>을 렌더링하세요. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수가 줄어듭니다.

쉽게 요약하면 아래와 같다.

  1. 먼저 props로 넘겨보자.
    • 여러 컴포넌트를 거쳐야 해도, 어떤 컴포넌트가 어떤 데이터를 사용하는지 명확하게 보여줄 수 있다.
  2. 필요 없는 중간 컴포넌트를 줄이자.
    • 데이터만 전달하고 쓰지 않는 컴포넌트가 많으면, 컴포넌트를 잘게 나누거나 children을 활용해서 데이터를 쓰는 컴포넌트와 가까운 구조로 만들 수 있다.

결국 “props로 자연스럽게 해결할 수 없고, 많은 컴포넌트가 같은 데이터를 써야 한다면 Context를 고민해보자.” 라고 정리할 수 있을 거 같다.

리액트 공식문서에서 예시로 든 Context 사용 방법은 아래와 같다.

  • 테마 지정하기
  • 로그인 정보
  • 라우팅
  • 상태 관리

실제 내 코드에 적용해보기

  1. 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수 줄이기

    <CardNumberSection
        cardNumber={cardNumber}
        cardNumberError={cardNumberError}
    />
    <CardNumberSection>
    	 <CardNumberInputs
          cardNumber={cardNumber}
          cardNumberError={cardNumberError}
       />
    </CardNumberSection>
  2. Context 사용하기

    • 카드 정보를 담는 Context 만들기
    import { createContext } from 'react';
    
    export const CardInfoContext = createContext(null);
    • provider로 상태 넘기기
    
    function App() {
      const { cardNumber, expirationPeriod, CVCNumber, password, cardType } =
        useAllCardInfo();
    
      return (
        <StyledApp>
          <StyledFrame>
            <CardPreview
              cardNumber={cardNumber.values}
              expirationPeriod={expirationPeriod.values}
              cardType={cardType.values.cardType}
            />
            <CardInfoForm
              cardNumber={cardNumber}
              expirationPeriod={expirationPeriod}
              CVCNumber={CVCNumber}
              password={password}
              cardType={cardType}
            />
          </StyledFrame>
        </StyledApp>
      );
    }
    function App() {
      const cardInfo = useAllCardInfo();
    
      return (
        <StyledApp>
          <StyledFrame>
           <CardInfoContext.**Provider** value={cardInfo}>
    	        <CardPreview />
    	        <CardInfoForm />
           **</CardInfoContext.Provider>**
          </StyledFrame>
        </StyledApp>
      );
    }

valuecardInfo ****전체를 넘기고 있어서, 만약 카드 번호만 변경되어도 cardInfo(유효 기간, 비밀 번호 등등..)를 구독하는 모든 컴포넌트가 리렌더링이 발생한다. 이는 매우 비효율적이다.

그러면 Context를 잘게 쪼개면 되는거 아니야? 라는 의문이 생긴다. 하지만 이렇게 되면 아래 코드와 같이 Provider 지옥이 발생한다.

<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <NotificationContext.Provider value={notifications}>
      <SettingsContext.Provider value={settings}>
        <LanguageContext.Provider value={language}>
          {/* 실제 화면 */}
        </LanguageContext.Provider>
      </SettingsContext.Provider>
    </NotificationContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

Context로 상태 관리?

상태(State)란 어플리케이션의 작동에 관여하는 모든 데이터를 말한다. 중요한 것은 데이터가 필요 시 저장되고(stored), 읽히고(read), 업데이트되고(updated), 사용되어야(used) 한다는 점이다.

그런 의미에서 상태 관리란

  • 초기 값 저장
  • 현재 값 읽기
  • 값 업데이트

를 의미한다고 할 수 있다. 일반적으로 현재 값이 변경되면 알리는 역할도 포함한다. 리액트의 useState와 useReducer 훅은 상태 관리의 좋은 예시가 된다. 

이런 맥락에서 봤을 때 Context는 상태 관리 툴이 아니다. Context는 그 자체로는 아무것도 '저장'하지 않는다. 상위 컴포넌트가 <MyContext.Provider>를 렌더하는 상위 컴포넌트는 Context에 어떤 값을 넣어줄지 결정하는 것에만 관여한다. 그리고 이 값은 보통 리액트 컴포넌트의 상태에 기반한다. 실질적인 '상태 관리'는 useState/useReducer 훅으로 이루어지는 것이다.

결국, 컨텍스트는 이미 어딘가에 존재하는 상태를 다른 컴포넌트와 공유하는 방법일 뿐이다.

그래서 Context 만을 사용해서는 실제로 상태를 관리할 수는 없다. 공식문서에서는 Context를 사용해 상태 관리하기 위해 Contextreducer를 함께 쓰기를 권장한다.

출처

https://velog.io/@woogur29/React-Context의-내부-동작-원리

https://velog.io/@jheeju/리액트-Context-는-상태-관리-도구가-아니다

https://www.npmjs.com/package/use-context-selector

profile
프론트엔드 개발하는 사람

0개의 댓글