Life is connecting the dots .

Context API - 리액트에서 데이터를 다루는 방법 본문

Programming/React

Context API - 리액트에서 데이터를 다루는 방법

soyeori 2023. 7. 25. 00:17

리액트에서는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 Props를 사용하여 전달한다. 또한 한 컴포넌트 안에서는 변경 가능한 데이터를 다루기 위해 State를 사용한다. 이렇게 props와 state를 사용하게 되면 부모 컴포넌트에서 자식 컴포넌트로 즉, 한쪽 방향으로만 데이터가 이동하게 된다.

 

Prop drilling

 

그렇다면 다음과 같이 서로 다른 데이터의 흐름에 있는 컴포넌트에서 서로 같은 데이터를 공유하고 싶은 경우에는 어떻게 하면 될까? 그럴 때는 공통 부모 컴포넌트에 state를 만들고 데이터를 props를 통해 사용하고자 하는 컴포넌트에 전달해야 한다. 하지만 이와 같은 props drilling은 컴포넌트가 여러 계층을 가지고 있는 복잡한 구조에서는 비효율적이고, 추적하기가 쉽지 않다.

Context API

이와 같은 문제를 해결하기 위해 리액트에서는 전역에서 데이터를 다룰 수 있는 Context API를 제공해 준다. Context API는 Flux패턴을 생각해 보면 이해하기 쉽다. Context는 리액트의 데이터 흐름과는 상관없이 전역으로 데이터를 관리할 수 있도록 한다. 즉, 전역 데이터를 공통 컴포넌트에 저장하고, 필요한 컴포넌트에서 해당 데이터를 호출해서 사용하면 된다.

 

Using context in distant children

사용 방법

Context API를 사용하는 단계는 다음과 같다.

  • 공통으로 사용할 데이터를 createContext를 사용하여 생성하고, 저장한다.
  • 저장한 데이터를 사용하기 위해 Provider를 사용하여 공통으로 사용할 컴포넌트를 감싸준다.
  • 전역 데이터를 사용할 컴포넌트에서 useContext를 사용하여 필요한 데이터를 호출하여 사용한다.

위의 단계를 차례대로 진행해서 '사용자 정보(이름과 메일)' 데이터를 main과 detail페이지에서 공유하는 코드를 작성해 보았다.

 

// src/context/index.tsx

import { createContext } from 'react';

interface Context {
  ...
}

// createContext 훅을 이용하여 Context 생성
export const UserContext = createContext<Context>({
  userName: '',
  userMail: '',
});

interface Props {
  children: JSX.Element | JSX.Element[];
}

// Provider를 사용하여 Context 안에 있는 값을 사용할 컴포넌트를 감싸주고,
// value에 여러 컴포넌트에서 공유 할 값을 넣어준다.
export function UserContextProvider({ children }: Props) {
  const userName = 'Sohyun';
  const userMail = 'test@gmail.com';

  return (
    <UserContext.Provider
      value={{
        userName,
        userMail,
      }}
    >
      {children}
    </UserContext.Provider>
  );
}

 

생성한 Context의 Provider와 Context를 외부에서 사용하기 위해 export 해주고, 공통 부모 컴포넌트에 위에서 생성한 UserContextProvider 컴포넌트를 감싸줌으로써 Provider를 제공해 준다.

 

// 여기서는 공통 부모 컴포넌트가 App.tsx

import styled from '@emotion/styled';
import { UserContextProvider } from 'context';
...

function App() {
  ...
  return (
    <Container>
      <UserContextProvider>
        <Routes>
          <Route path="/main" element={<MainPage />}></Route>
          <Route path="/detail" element={<DetailPage />}></Route>
        </Routes>
        ...
      </UserContextProvider>
      ...
    </Container>
  );
}

export default App;

 

전역 데이터를 사용할 컴포넌트에서 useContext 훅을 사용해서 Context 데이터를 사용한다. 최종 메인페이지와 디테일 페이지에 접속했을 때, 전역데이터로 공유된 사용자 이름과 사용자 메일이 페이지에 보이는 것을 확인할 수 있었다.

 

// pages/main/index.tsx
import { useContext } from 'react';
import { UserContext } from 'context';
...
export default function MainPage() {
  const { userName } = useContext(UserContext);
  return (
    <Container>
      <Title>{userName} 님 안녕하세요!</Title>
    </Container>
  );
}

// pages/detail/index.tsx
import { useContext } from 'react';
import { UserContext } from 'context';
...
export default function DetailPage() {
  const { userName, userMail } = useContext(UserContext);
  return (
    <Container>
      <Title>{userName} 님! 메일 주소를 확인해주세요.</Title>
      <Contents>메일 주소: {userMail}</Contents>
    </Container>
  );
}

마무리

Context API를 한 번 사용해 보니 조금 더 복잡한 계층구조를 가진 컴포넌트에서 더 유용하게 사용할 수 있고, useState를 사용하여 변하는 값을 넣거나 공유할 함수 등을 Context에 넣어서 간단한 앱을 만들 때 시도해 볼 수 있을 것 같다.

또한 Context API를 사용하면서 느낀 점은 사용법이 마치 Recoil 라이브러리와 비슷한 느낌을 받았다. Recoil 라이브러리도 RecoilRoot를 사용하여 다른 컴포넌트들을 감싸주는 셋팅이 필요하고, 전역으로 사용할 데이터를 atom 또는 selector로 생성해서 필요한 곳에서 호출해서 사용하면 된다. 

 

// _app.tsx (Next.js)
import { RecoilRoot } from "recoil";

export default function App({ Component, pageProps }: AppProps) {
  return (
      <RecoilRoot>
            ...
              <Component {...pageProps} />
            ...
      </RecoilRoot>
  );
}

 

이처럼 Context API는 간편하게 사용할 수 있다 보니 그만큼 남용돼서 사용하지 않도록 주의해야 하고, 공식문서에서는 다음과 같이 Context API를 사용하기 전에 고려해야 할 사항에 대해 알려주고 있다.

 

Just because you need to pass some props several levels deep doesn’t mean you should put that information into context.
→ props를 여러 단계에 걸쳐서 깊이 전달해야 한다 ≠ 해당 정보를 context에 넣어야 한다.

 

  • props 사용하기 : 수십 개의 컴포넌트에 대해 각각의 props를 전달하는 것은 번거로운 작업일 수도 있지만 오히려 어떤 컴포넌트에 어떤 데이터가 사용되는지를 명확하게 보여주며 데이터의 흐름을 명시적으로 확인할 수 있다. 즉, 데이터 흐름을 확인할 수 있으므로 코드를 유지보수하기 용이하고 이해하기 더 쉬울 수 있다.

  • 컴포넌트를 별도로 분리하고, JSX를 자식으로 전달하기 : 만약 전달해야 하는 데이터가 그 해당 데이터를 사용하지 않는 컴포넌트층(layers)을 통과해서 더 하위로 전달해야 하는 경우, 예를 들어, 레이아웃을 만들 때 children props를 전달받아서 사용하면, 데이터를 지정하는 컴포넌트와 필요한 컴포넌트 사이의 계층 수가 줄어든다. 그렇게 되면 데이터 흐름이 더 명확해지고, 컴포넌트들 사이의 의존성이 줄어들게 된다.

// JSX

// "Layout" 컴포넌트에게 "posts" 데이터를 직접 전달하는 경우
<Layout posts={posts} />

// "Layout" 컴포넌트가 "children"을 prop으로 받고, "Posts" 컴포넌트를 렌더링하는 경우
<Layout>
  <Posts posts={posts} />
</Layout>

 

위와 같이 Context API를 사용하기 전에 다른 방법들이 좋은 대안으로 고려될 수 있다. 이처럼 React에서 데이터를 관리하는 방법을 적절히 활용하여 코드를 설계하는 것이 결국 데이터를 효율적으로 전달하고, 데이터 흐름을 더 명확하게 관리할 수 있는 방법이라고 생각한다.


참고

공식문서