본문 바로가기
코딩 공부/JavaScript

[React] React Context

by 현장 2023. 11. 29.

React Context

리액트 context는 앱에서 컴포넌트에게 props를 사용하지 않고 필요한 데이터를 넘겨주며 사용할 수 있게 해 줍니다. 다시 말해, 리액트 context는 컴포넌트들이 데이터(state)를 더 쉽게 공유할 수 있도록 해줍니다.

🏷️ 사용해야 할 때

리액트 context는 앱의 모든 컴포넌트에서 사용할 수 있는 데이터를 전달할 때 유용합니다.

✅ 데이터 종류

▪️ 테마 데이터 (다크 모드 혹은 라이트 모드)
▪️ 사용자 데이터 (현재 인증된 사용자)
▪️ 로케일 데이터 (언어 혹은 지역)

 

데이터는 자주 업데이트할 필요가 없는 리액트 context에 위치해야 합니다. 왜냐하면 context는 전체적인 상태 관리를 위해 만들어진 시스템이 아니기 때문입니다.

 

context는 데이터를 쉽게 사용하기 위해 만들어졌습니다. 즉, 리액트 context는 리액트 컴포넌트를 위한 전역 변수와 같다고 생각하면 됩니다.

🏷️ React Context가 해결해주는 문제

리액트 context는 props drilling을 막는 데 도움을 줍니다.

✔️ Props drilling

중첩된 여러 계층의 컴포넌트에게 props를 전달해 주는 것을 의미하는 데 사용하지 않는 컴포넌트들에게까지 전달을 하게되는 것을 말합니다.

export default function App({ theme }) {
  return (
    <>
      <Header theme={theme} />
      <Main theme={theme} />
      <Sidebar theme={theme} />
      <Footer theme={theme} />
    </>
  );
}
function Header({ theme }) {
  return (
    <>
      <User theme={theme} />
      <Login theme={theme} />
      <Menu theme={theme} />
    </>
  );
}

 

위 예시를 보면 이 앱에서 모든 컴포넌트에 prop으로 전달된 theme 데이터에 접근할 수 있습니다. 하지만 보시다시피 Header와 같은 App의 직계 자식 컴포넌트 또한 props를 사용해 테마 데이터를 내려주어야 합니다.

 

이 예제의 문제는 theme prop을 사용하지 않는 많은 컴포넌트들에게까지 해당 prop을 내려주고 있다는 점입니다. Header 컴포넌트는 자식 컴포넌트에게 theme을 내려주기만 할 뿐 직접 필요로 하지 않습니다. 다시 말해, User, Login, Menu 컴포넌트가 theme 데이터를 직접 사용하는 게 더 나을 것입니다. 이렇듯 모든 곳에 props를 사용하는 것을 우회할 수 있어 props drilling 문제를 피할 수 있는 것이 리액트 context의 장점입니다.

🏷️ React context 사용 방법

context는 리액트 버전 16부터 사용 가능한 리액트 내장 API입니다. 즉, 어떤 리액트 프로젝트라도 버전이 16이상인 리액트를 import하면 context를 바로 생성하고 사용할 수 있습니다.

✅ 리액트 context를 사용하기 위한 단계

1. createContext 메서드를 사용해 context를 생성합니다.
2. 생성된 context를 가지고 context provider로 컴포넌트 트리를 감쌉니다.
3. value prop을 사용해 context provider에 원하는 값을 입력합니다.
4. context consumer를 통해 필요한 컴포넌트에서 그 값을 불러옵니다.

✅ 예시

import React from 'react';

// 1: App 컴포넌트에서 createContext()를 사용해 
//    context를 만들고 UserContext 변수에 결과를 담습니다.
//    eact.createContext()를 사용하면 value prop에 초깃값을 정해줄 수 있다는 점에 유의하세요.
export const UserContext = React.createContext();

// 2: App 컴포넌트에서 UserContext를 사용합니다. 
export default function App() {
  return (
    // 3: UserContext.Provider에는 컴포넌트 트리 전체에 전달해주고 싶은 값을 넣습니다.
    <UserContext.Provider value="Reed">
      <User />
    </UserContext.Provider>
  )
}
function User() {
  return (
    // 4: User 혹은 context에서 제공하는 값을 사용하고 싶은 컴포넌트에서는 
    //    UserContext.Consumer라는 consumer 컴포넌트를 사용해야 합니다.(render props 패턴)
    <UserContext.Consumer>
      {value => <h1>{value}</h1>} 
      {/* prints: Reed */}
    </UserContext.Consumer>
  )
}

🏷️ useContext 훅

위의 예시를 보면 context를 사용하기 위해 render props 패턴을 사용하는 것이 조금 이상해 보일 수 있습니다. 하지만 리액트 훅이 도입된 리액트 16.8부터는 context를 사용하는 다른 방법이 등장했습니다. 이제는 도입된 useContext 훅을 사용해 context를 사용할 수 있습니다.

import React from 'react';

export const UserContext = React.createContext();
export default function App() {
  return (
    <UserContext.Provider value="Reed">
      <User />
    </UserContext.Provider>
  )
}
function User() {
  const value = React.useContext(UserContext);  
    
  return <h1>{value}</h1>;
}

위 예시와 같이 render props를 사용하는 대신, context를 사용할 때 컴포넌트 최상단에서 React.useContext()에 전체 context 객체를 내려줄 수 있습니다.

 

useContext 훅의 장점은 컴포넌트가 더욱 간결해지고 나만의 커스텀 훅을 만들 수 있다는 점입니다. 선호하는 패턴에 따라서 consumer 컴포넌트를 직접 사용할 수도 있고 useContext 훅을 사용할 수도 있습니다.

🏷️ context가 필요 없을 때

많은 개발자들이 하는 실수는 여러 레벨의 컴포넌트에 props를 내려줘야 하는 경우에 매번 context를 사용하는 것입니다. 

export default function App({ user }) {
  const { username, avatarSrc } = user;
  return (
    <main>
      <Navbar username={username} avatarSrc={avatarSrc} />
    </main>
  );
}
function Navbar({ username, avatarSrc }) {
  return (
    <nav>
      <Avatar username={username} avatarSrc={avatarSrc} />
    </nav>
  );
}
function Avatar({ username, avatarSrc }) {
  return <img src={avatarSrc} alt={username} />;
}

여기 App 컴포넌트의 username과 avatarSrc 두 가지 props가 필요한 Avatar라는 중첩 컴포넌트가 있습니다.

가능하다면 props가 필요 없는 컴포넌트들에게 여러 개의 props를 굳이 전달하고 싶지 않을 것입니다.

 

이 때에는 prop drilling 하고 있다는 이유로 바로 context에 의지하는 것보다는 컴포넌트를 더 잘 구성함으로써 해결할 수 있습니다. 최상위 컴포넌트인 App만 Avatar 컴포넌트에 대해 알고 있으면 되기 때문에 App 내에 바로 avatar를 만드는 것입니다. 이렇게 하면 두 개의 props를 내려주는 것 대신 avatar라는 하나의 prop만 내려주면 됩니다.

export default function App({ user }) {
  const { username, avatarSrc } = user;
  const avatar = <img src={avatarSrc} alt={username} />;
  return (
    <main>
      <Navbar avatar={avatar} />
    </main>
  );
}
function Navbar({ avatar }) {
  return <nav>{avatar}</nav>;
}

요약하자면, 바로 context를 사용하지는 말고 prop drilling을 피하기 위해 컴포넌트를 개선하여 설계할 수 있는지를 먼저 살펴보는게 좋습니다.

🏷️ context 사용 시 주의사항

리액트 context를 useReducer와 같은 훅과 결합해 서드파티 라이브러리 없이 임시 상태 관리 라이브러리를 생성하는 것이 가능하지만, 보통 성능상의 문제로 권장되지 않습니다. 이런 접근법은 리액트 context가 재렌더링을 초래한다는 문제가 있습니다.

 

만약 리액트 context provider에 객체를 내려주고 있고 그 객체의 프로퍼티가 업데이트된다면 그 context를 사용하고 있는 모든 컴포넌트에서 재렌더링이 일어날 것입니다.

 

state가 거의 없는 작은 앱의 경우와 같이 업데이트가 자주 되지 않는 경우라면 이러한 성능 이슈가 일어나지 않을 것입니다.  하지만 트리의 많은 컴포넌트에서 state 변경이 자주 일어난다면 문제가 되니 주의해야 합니다.

📖 Reference

freeCodeCamp

'코딩 공부 > JavaScript' 카테고리의 다른 글

[React] Formik  (0) 2023.12.03
[React] useEffect  (0) 2023.12.02
[React] Default Export와 Named Export  (2) 2023.11.26
[React] React.js  (0) 2023.11.24
[JS] Axios  (0) 2022.12.11