본문 바로가기

Web.d

[React][Context] Prop drilling 과 useContext (2/2)

반응형

https://snupi.tistory.com/185

 

[React][Context] Prop drilling 과 useContext (1/2)

Prop Drilling (프로퍼티 내리꽂기) Typescript를 공부하며, Prop interface의 중복을 모듈화 하여 해소할 수 있을까 했는데, 애초에 많이 중복된다는 것은, prop drilling 문제의 가능성이 있을 수 있다고 하여.

snupi.tistory.com

 


useContext

useState, useEffect와 함께 기본 Hook인 useContext에 대해 알아봅시다.

Context 개념

 

Context 란?

 

그러한 데이터로는 현재 로그인한 유저, 테마, 선호하는 언어, 데이터 캐시 등이 있습니다.

 

// context를 사용하면 모든 컴포넌트를 일일이 통하지 않고도
// 원하는 값을 컴포넌트 트리 깊숙한 곳까지 보낼 수 있습니다.
// light를 기본값으로 하는 테마 context를 만들어 봅시다.
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Provider를 이용해 하위 트리에 테마 값을 보내줍니다.
    // 아무리 깊숙히 있어도, 모든 컴포넌트가 이 값을 읽을 수 있습니다.
    // 아래 예시에서는 dark를 현재 선택된 테마 값으로 보내고 있습니다.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 이젠 중간에 있는 컴포넌트가 일일이 테마를 넘겨줄 필요가 없습니다.
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 현재 선택된 테마 값을 읽기 위해 contextType을 지정합니다.
  // React는 가장 가까이 있는 테마 Provider를 찾아 그 값을 사용할 것입니다.
  // 이 예시에서 현재 선택된 테마는 dark입니다.
  static contextType = ThemeContext;
  
  render() {
    return <Button theme={this.context} />;
  }
}

 

사용 형태로 보아, 우리가 흔히 사용하던 ThemeProvider 역시 Context API 를 사용하는 것을 유추할 수 있습니다.

 

import { ThemeProvider } from "styled-components";
import theme from "./styles/theme";
import Router from "components/Router";

const App = () => {
  return (
    <ThemeProvider theme={theme}>
      <Router />
    </ThemeProvider>
  );
};

export default App;

 

API 사용

  • React.createContextContext
const SnupiContext = React.createContext(defaultValue);

Context 객체를 만든다.

React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 값을 읽는다.

즉, 상위 Provider보다 하위 Provider의 값이 우선시된다.

찾지 못했을 때만, defaultValue 값이 쓰인다.

 

  • Context.ProviderContext
<SnupiContext.Provider value={/* 어떤 값 */}>

Context 객체에 포함된 Provider는 context의 변화를 구독하는 컴포넌트들에게 알린다.

Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 된다.

Provider로부터 하위 consumer로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트 된다.

 

  • Context.Consumer
<SnupiContext.Consumer>
  {value => /* context 값을 이용한 렌더링 */}
</SnupiContext.Consumer>

Consumer의 자식으로는 함수가 와야 한다.

이 함수는 context의 현재 값(value)을 받고 React 노드를 반환하게 된다.

 

  • ~~Context.displayName~~
  • ~~Class.contextType~~

대체 방법: Inversion of Control / Render Props

 

 

API 사용법

애플리케이션의 데이터 저장소인 context는 다음과 같이 만든다.

 

// contexts/index.js

import React from "react";

export const SnupiContext = React.createContext();
// contexts/ContentProvier.jsx

import React, { useState } from "react";
import { SnupiContext } from ".";

function UserContextProvider({ children }) {
  const [user, setUser] = useState({
    name: "Snupi",
    loggedIn: false,
  });
  const logUserIn = () => setUser({ ...user, loggedIn: true });
  return (
    <SnupiContext.Provider value={{ user, logUserIn }}>
      {children}
    </SnupiContext.Provider>
  );
};

export default UserContextProvider;

 

  1. React.createContext() 메소드로 context를 생성하고,
  2. <UserContext.Provider value={{ ~ }}> state 값을 value로 가진 태그 안에
  3. {children} 을 넣어 리턴한다.

이후에 App.jsx에서는

 

 

// App.jsx

import Router from "./Components/Router";
import UserContextProvider from "./contexts/ContextProvider";

function App() {
  return (
    <UserContextProvider>
      <Router />
    </UserContextProvider>
  );
}

export default App;

 

위와 같이 나타낼 컴포넌트를 UserContext.Provider 태그 안에 {children}으로 넣어준다.

이후 컴포넌트 안에서는 react의 { useContext } 모듈과, context.js에서 export한 { userContext } 모듈로 다음과 같이 사용한다.

 

const {
    user: { name, loggedIn }
} = useContext(UserContext);

// or

const { logUserIn } = useContext(UserContext);

 

쓸 때마다 useContext를 사용할 필요 없이, 다음과 같이 contexts 한 파일에서 자동으로 선언해 줄 function을 만들어 줄 수 있다.

 

// contexts/index.js

import React, { useContext } from "react";

export const SnupiContext = React.createContext();

export const useUser = () => {
  const { user } = useContext(SnupiContext);
  return user;
};

export const useFunction = () => {
  const { logUserIn } = useContext(SnupiContext);
  return logUserIn;
};

 

이를 통해 다른 파일에서 다음과 같이 useContext(userContext)를 쓰지 않고 함수로 선언해 줄 수 있다.

 

const { name, loggedIn } = useUser();
const { logUserIn } = useFunction();
// context 사용 컴포넌트

import React from "react";
import { useFunction, useUser } from "../contexts";

const PrtContext = () => {
  const { name, loggedIn } = useUser();
  const logUserIn = useFunction();
  return (
    <div>
      <div>
        Hello, {name}, you are {loggedIn ? "logged in" : "anonymous"}
      </div>
      <button onClick={logUserIn}>Log user in</button>
    </div>
  );
};

export default PrtContext;

 

위와 같이 많은 데이터를 처리해야 할 때면 데이터를 변경하는 function도 많아지므로,

데이터를 제공하는 Data Provider, 데이터를 변경하는 Provider2개 나누는 것이 좋다.

또는 데이터를 종류 별로 나누는 것도 좋다.

 

마지막으로, context 값이 변경됨으로써 리렌더링 하는 것에 비용이 많이 든다면, useMemo 등의 메모이제이션을 사용하여 최적화할 수 있다. (https://github.com/facebook/react/issues/15156#issuecomment-474590693)

장점

Redux와 비교하여, 다음과 같은 장점이 있다.

  • Built-in
  • 러닝 커브가 낮다
  • 전역 상태가 아닌, Single State Tree에서 유용하다

  • Lifecycle 관리가 유용하다
    • Context는 Redux와 달리, React 컴포넌트 트리 상에 존재하기 때문에, Provider 컴포넌트가 unmount 되면 함께 소멸된다.

단점

  • React 컴포넌트이기 때문에 생길 수 있는 Wrapper Hell

  • 컨벤션/편의 도구의 부재
  • Context를 사용하게 되면 컴포넌트를 재사용하기가 매우 힘들어지기 때문에, 마구 사용하는 건 지양해야 한다
  • 이 외에도 치명적인 단점으로 "Context API 렌더링 최적화" 가 힘들다는 단점이 있다
    이 때문에 props 의 변화가 없음에도 리렌더링이 이루어질 수 있고,
    디버깅조차 힘들어지는 경우가 생길 수 있다
    알고 싶지 않았는데,,,깨닫게 되었다,,....
반응형