본문 바로가기
JS/React 강의

[React] 15. 리액트 Context API란 무엇일까?

by GiraffePark 2023. 12. 4.

 

안녕하세요. 박기린 입니다.

이번엔 React의 state 관리 방법 중 하나인 Context API에 대해 알아보겠습니다.

 

 

 


Context API (w. 기존 props 방식의 한계)

리액트는 props라는 걸 통해서 여러 리액트 컴포넌트를 걸쳐 state 데이터를 전달합니다.

 

 

리액트 앱이 위와 같은 구조로 이루어져 있다고 가정을 합니다.

LoginForm에 있는 Login state를 장바구니 Cart 컴포넌트에서 접근하려고 합니다. 하지만 바로 접근하는 것은 불가능합니다.

 

 

 

 

 

 

위 사진처럼 props를 이용해서 root 컴포넌트로 데이터를 올린 다음, 밑으로 다시 뿌립니다.

이 과정 속에서, 해당 state 데이터를 필요로 하지 않는 다른 컴포넌트들도 지나가야만 하는 상황이 발생합니다.

코드와 구조가 복잡해지는 번거롭고 비효율적인 방식으로 보입니다.

 

 

이 문제를 해결하기 위해,

1. 컴포넌트 전역에서 사용할 수 있는

2. 리액트에 기본 내장된 (외부 라이브러리가 아닌)

state 저장소가 있습니다.

그것이 바로 React Context입니다.

 

 

 

 

길고 복잡한 props 체인 대신, 컴포넌트 전체에서 접근이 가능한 하나의 state 저장소가 있습니다.

그래서 원하는 컴포넌트만 이 저장소에 접근해서 원하는 데이터를 접근할 수 있습니다.

 

 

 

 

 


Context 파일 만들기

Context를 사용하기 위해서, src 폴더에 store 폴더를 추가합니다.

(context, state 등 다른 이름으로 지어도 됩니다.)

 

 

 

 

 

만든 폴더 안에, context를 관리할 js 파일을 추가합니다.

 

예시로 로그인-인증 state를 몇 개를 관리할 것이기에 auth-context.js를 추가했습니다.

AuthContext같이 UpperCamelCase로 적어도 상관은 없지만, 리액트 컴포넌트 파일의 이름도 UpperCamelCase로 짓는 관행이 있다보니 둘이 겹쳐서 착각할 수 있습니다. 그래서 케밥 표기법을 사용했습니다.

 

auth-context.js 뿐만 아니라 여러 종류의 Context를 가질 수 있습니다. products-context.js 파일을 추가해서 여러 개의 Context를 한 앱에서 사용할 수 있습니다.

(반대로 거대한 하나의 context를 만든 후 모든 state를 이 안에 담는 방식으로 사용할 수도 있습니다. 오로지 개발자 마음)

 

 

 

이제 auth-context.js파일을 엽니다.

// auth-context.js
const AuthContext = React.createContext({
  isLoggedIn: false,
});

export default AuthContext;

 

리액트 라이브러리에서 createContext()를 호출합니다. 이 함수는 말 그대로 context를 생성합니다.

 

이름을 AuthContext라고 지은 이유 : 파일 이름은 케밥 표기법이고, UpperCamelCase는 컴포넌트에만 쓰기로 약속된 게 아니냐고 할 수 있습니다. 실제로 AuthContext 자체는 컴포넌트가 아닌 상태이지만, 컴포넌트를 포함할 객체이기 때문에 동일하게 UpperCamelCase를 사용했습니다. (컴포넌트를 포함한다는 말의 의미는 다음 섹션에서 바로 확인할 수 있습니다.)

 

그리고 이 AuthContext를 다른 파일에서도 사용할 수 있게 끔 export 해줍니다.

 

그런 다음에 리액트 앱에서 context를 사용하려면 두 가지 작업을 수행해야 합니다.

먼저 공급해야 합니다. 리액트에게 '여기에 내 컨텍스트가 있어'라고 알려줘야 합니다.

그 다음에 소비해야 합니다. 컨텍스트와 연동하고 리스닝해야 합니다.

 

 

 


Context를 앱에 공급하기

'공급'한다는 것은 context를 활용해야 하는 모든 컴포넌트를 JSX코드로 감싸는 것을 의미합니다. 그래서 해당 컨텍스트를 리스닝할 수 있게 해줘야 합니다. 감싸지지 않으면 컨텍스트에 접근할 수 없습니다.

아래의 코드를 보면 이 말이 무슨 뜻인지 알 수 있습니다.

 

 

    <AuthContext.Provider
      value={{
        isLoggedIn: isLoggedIn,
        onLogout: logoutHandler,
      }}
    >      
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </AuthContext.Provider>

 

AuthContext에 접근해야 하는 컴포넌트를

<AuthContext.Provider> {...} </AuthContext.Provider>

위 태그 안에 모두 집어넣습니다. 그러면 AuthContext에 접근 가능해집니다.

심지어 모든 자식 컴포넌트까지 다함께 AuthContext에 접근 가능해집니다.

 

.Provider를 붙이는 이유 : 위에서 말씀드린 것처럼 context 자체는 컴포넌트가 되지 않습니다. 하지만 <JSX> 구문은 항상 컴포넌트 안에서만 작성할 수 있습니다. 그래서 context를 컴포넌트로 변환해주는 역할을 Provider가 합니다. (context를 공급한다는 표현도 provider에서 따왔습니다.)

 

만약에 App 전역에서 사용해야 하는 Context라면, Root 컴포넌트인 <App/> 컴포넌트를 <AuthContext.Provider>로 감싸준 다음에 초기값만 지정해주면 됩니다.

 

 


Context를 앱에서 소비하기

Provider를 통해 앱은 Context를 공급받습니다. 이제 이 context와 연동을 해야합니다. 이걸 '리스닝'한다고 표현합니다.

 

리스닝을 하는 방법은 두 가지가 있습니다.

1. context의 consumer를 사용합니다.

2. React Hooks를 사용합니다.

 

 

 

1. consumer를 사용하는 방법

context를 사용할 컴포넌트에 Consumer 컴포넌트를 감싸줍니다.

const Navigation = (props) => {
  return (
    <AuthContext.Consumer>
      {(ctx) => {
        return (
          <nav className={classes.nav}>
            <ul>
              {ctx.isLoggedIn && (
                <li>
                  <a href="/">Users</a>
                </li>
              )}
              {ctx.isLoggedIn && (
                <li>
                  <a href="/">Admin</a>
                </li>
              )}
              {ctx.isLoggedIn && (
                <li>
                  <button onClick={ctx.onLogout}>Logout</button>
                </li>
              )}
            </ul>
          </nav>
        );
      }}
    </AuthContext.Consumer>
  );
};
// 로그인 상태이면 위의 Navigation 바에 'Users', 'Admin' 링크와 'Logout' 버튼이 생긴다.

아까 <AuthContext.Provider>처럼,

<AuthContext.Consumer> {...} </AuthContext.Consumer>

라는 태그를 JSX에 사용합니다.

 

 

 

<AuthContext.Consumer> 태그 내부에 {중괄호}를 넣어 JSX 코드를 감싸줄 준비를 한 후, 이 안에 함수를 넣어줍니다.

 <AuthContext.Consumer>
      {(ctx) => {

이 함수는 Consumer가 사용된 AuthContext의 state를 인수로 받습니다. (위 코드의 경우 ctx 안에 담깁니다.)

 

 

 

    <AuthContext.Consumer>
      {(ctx) => {
        return (
          <nav className={classes.nav}>
            <ul>
              {ctx.isLoggedIn && (
                <li>
                  <a href="/">Users</a>
                </li>
              )}
              {ctx.isLoggedIn && (
                <li>
                  <a href="/">Admin</a>
                </li>
              )}
              {ctx.isLoggedIn && (
                <li>
                  <button onClick={ctx.onLogout}>Logout</button>
                </li>
              )}
            </ul>
          </nav>
        );
      }}
    </AuthContext.Consumer>

그리고 다시 함수에 return 키워드를 사용한 후, 원래 있던 컴포넌트를 적어줍니다.

 

이게 바로 Consumer 방식입니다. 근데 뭔가 복잡해보이는 방식입니다.

 

 

 

 

 

2. useContext를 사용하는 방법

이번에는 React Hook을 사용합니다.

 

 

import React, { useContext } from "react";

useContext를 리액트 라이브러리에서 import합니다.

리액트에 내장된 훅으로, context를 활용하고 리스닝할 수 있게 해줍니다.

 

 

 

 

  const ctx = useContext(AuthContext)

리액트 컴포넌트 함수 안에 useContext()를 호출한 후, 컨텍스트의 포인터를 전달합니다. (AuthContext를 import 한 후 그대로 함수 안에 넣어주면 됩니다.)

그리고 useContext()가 실행된 데이터를 따로 상수에 저장해서, (위에서 본) Consumer 컴포넌트를 사용했던 것과 동일하게 사용할 수 있습니다.

 

 

 

import React, { useContext } from "react";
import AuthContext from "../../store/auth-context";
import classes from "./Navigation.module.css";

const Navigation = (props) => {

  const ctx = useContext(AuthContext)

  return (
    <nav className={classes.nav}>
      <ul>
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Admin</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <button onClick={props.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

export default Navigation;

Consumer 컴포넌트를 사용했을 때보다 훨씬 더 간단해보입니다만, 사람마다 차이가 있을 수 있습니다.

둘 다 기능은 동일하니, 마음에 드는 방식으로 사용하시면 됩니다.

 

 

 

 

 


제한사항

컨텍스트는 App 전역에서 사용할 수 있어 무척이나 편하고 좋아보입니다.

그래서 모든 State를 컨텍스트로만 관리하려고 할 수 있는데, 몇 가지 제한되는 점이 있습니다.

 

 

1. 재사용되는 컴포넌트는 props를 사용해야 합니다.

const Button = (props) => {
  return (
    <button
      type={props.type || 'button'}
      className={`${classes.button} ${props.className}`}
      onClick={props.onClick}
      disabled={props.disabled}
    >
      {props.children}
    </button>
  );
};

export default Button;

위의 Button 클래스를 비롯하여, 다양한 state에 맞춰 재사용될 수 있는 컴포넌트들은 props를 사용해야 합니다.

props 대신에 AuthContext 같은 걸 넣었다가는, 오로지 로그인 기능만 구현하는 버튼이 될 것입니다.

 

 

 

 

2. 변경주기가 잦은 state는 context 사용에 부적합합니다. (ex: 초단위로 state가 갱신됨.)

공식 리액트 팀원이 한 말입니다.

그렇다면 앱 전역에 쓰이는 동시에, 변경주기가 잦은 state는 무엇으로 관리해야 할까 고민이 됩니다.

 

이때 쓸 수 있는 도구가 바로 Redux입니다.

Redux에 대해서는 다음에 알아보겠습니다.

 

 

 

 

3. 그리고 되도록 props를 사용합시다.

props를 완전히 대체하기 위해 context를 사용하는 것은 부적절합니다.

오히려 context보다 props를 사용하는 경우 더 간단하고 직관적으로 해결이 되는 경우가 있습니다. (ex: 체인이 단순하거나 짧은 경우)

context는 오로지 props의 연결이 길어질 때만 사용합니다.

 

 

반응형