본문 바로가기

Frontend Study - 2/React

React : Context API - props 없이 state 공유 하기.

 

Context API의 활용.

1) 어플리케이션 전반적으로 모든 컴포넌트들이 필요한 상태값의 공유 ( ex) 다크모드, 언어, 로그인유무)

2) 거리가 먼 컴포넌트들 간의 상태값 공유. ex) G컴포넌트와 J컴포넌트가 prop drilling 없이 상태값을 공유할 수 있게 한다.

 

이 때 중요한 것은 '무조건' Root Component에 Context를 심으면 안된다는 것이다.

Context API는 렌더링을 발생시킨다. 상태값이 변화할 때 상태값을 사용하지 않는 컴포넌트들도 같이 재렌더링 되기 때문에 무분별한 사용은 지양해야 한다. 그렇기 때문에 빈번히 업데이트 되는 상태라면 Context API를 사용하지 않는 것이 좋다. 

 

- 최대한 효율적으로 사용해야 한다. 예를 들면 B컴포넌트와 C컴포넌트가 공유해야할 상태값이 있을 때는 A컴포넌트에 Context API를 심는 것. 데이터가 어디까지 공유되어야 하는지를 확인해서 사용해야 한다.

 

 

 

사용법 예시

 

1) createContext()를 통해서 context를 만들어 준다. Context는 state 보관함이라고 생각하면 된다.

import { createContext, useState } from 'react';

export const DarkModeContext = createContext();

export function DarkModeProvider({ children }) {
  const [darkMode, setDarkMode] = useState(false);
  const toggleDarkMode = () => setDarkMode((mode) => !mode);
  return (
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
}

 

2) 만들어준 Context(darkModeContext)로 값을 사용할 컴포넌트를 감싸준다.  App.js에서 선언한 state를 value로 넣어서 전달해주는 거라고 생각하면 된다. 이제 Header, Main, Footer 컴포넌트의 자식의 자식의 자식 컴포넌트도 props전달 없이 Context의 상태값을  쓸 수 있게 되는 것.

 

3) state를 사용할 컴포넌트에서 만들어둔 Context를 import 해온다.

 (import {dakrModeContext, DarkModeProvider} from './../...';)

 

4) useContext를 Import 해온 뒤에, 3)에서 import해온 context를 넣어서 호출해준다.  

stockContext에 넣어두었던 state가 그 자리에 전부 남게 된다.

구조분해 할당하고 사용해준다.

import React, { useContext } from 'react';
import { DarkModeContext, DarkModeProvider } from './context/DarkModeContext';

export default function AppTheme() {
  return (
    <DarkModeProvider>
      <Header />
      <Main />
      <Footer />
    </DarkModeProvider>
  );
}

//

function Main() {
  const { darkMode, toggleDarkMode } = useContext(DarkModeContext);
  return (
    <div>
      Product Detail
      <p>
        DarkMode:
        {darkMode ? (
          <span style={{ backgroundColor: 'black', color: 'white' }}>
            Dark Mode
          </span>
        ) : (
          <span>Light Mode</span>
        )}
      </p>
      <button onClick={() => toggleDarkMode()}>Toggle</button>
    </div>
  );
}

 

이 때 !

useDarkMode라는 hook을 만들어서 사용해주면,

dark모드를 사용하는 컴포넌트에서 일일이 useContext와 DarkmodeContext를 호출해 주지 않아도 된다.

import { createContext, useState, useContext } from 'react';

export const DarkModeContext = createContext();

export function DarkModeProvider({ children }) {
  const [darkMode, setDarkMode] = useState(false);
  const toggleDarkMode = () => setDarkMode((mode) => !mode);
  return (
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
}

export const useDarkMode = () => useContext(DarkModeContext);

 

 

- 또 다른 예시.

export let stockContext = React.createContext();

function App(){
  let [stocks, setStocks] = useState([10,11,12]);

	return ( 
     
     <Product />
     <Detail />
     
     }

}

 

export let stockContext = React.createContext();

function App(){
  let [stocks, setStocks] = useState([10,11,12]);

	return ( 
    <stockContext.Provider value={{stocks}}>
    	<Product />
     	<Detail />
    </stockContext.Provider>
     }

}

 

2. state 꺼내서 사용하기

(Detail.js)

import {useState, useContext} from 'react';
import {stockContext} from './../App.js';

function Detail(){
  let {stocks} = useContext(stockContext)

  return (
    <div>{stocks}</div>
  )
}

 

 

- context API의 무분별한 사용은 경계해야 한다.

 

 

1) 필요 이상의 렌더링을 발생 시킨다는 성능이슈가 있고,

 

-> 예컨데 아래의 경우 stocks - state 값이 변경되었다고 하면 그 안에 stocks라는 state를 사용하지 않는 컴포넌트들도 있을 것인데 다같이 재렌더링 되게 된다.

    <stockContext.Provider value={{stocks}}>
    	<Product />
     	<Detail />
    </stockContext.Provider>

 이를 방지하기 위해서 아래와 같은 방법을 사용할 수도 있다.

const Detail = React.memo(() => <div>hello react!</div>);

React.memo의 두번째 인자로 아무것도 반환하지 않는다면 자체적으로 props를 비교해서 렌더링 여부를 결정하고, 아니면 두번째 인자에 값을 줌으로써 렌더링 여부 기준을 직접 작성할 수도 있다. 하지만 그렇다고 memo를 무분별하게 남발하는것 또한 좋지 않다. memo의 렌더링 조건(props의 변화 or dependency 값 변화) 비교 연산과 리렌더링 중 어느 쪽이 퍼포먼스 최적화에 더 적합한지 확인이 필요하기 때문.

 

 

2) context로 받아온 값을 사용한 컴포넌트는 재사용이 어려워 지게 된다는 단점이 있다.

 

상황에 따라 Context API를 사용하는 방법보다, component 합성이 더 쉬운 해결책일 수 있다.

 

<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

prop drilling이 일어나고 있는 상황. 실제로 사용되는 곳은 Avatar 컴포넌트 뿐인데 user와 avatarSize props를 여러 단계에 걸쳐 보내주고 있다. 

 

이 때 아래와 같이 Avatar 컴포넌트 자체를 넘겨주면 context를 사용하지 않고 이를 해결할 수 있다.

그러면 중간에 있는 컴포넌트들이 user나 avatarSize 에 대해 전혀 알 필요가 없습니다.

 

  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 이제 이렇게 쓸 수 있습니다.
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}

 

 

하지만 이러한 단순한 구조가 아닌,

같은 데이터를 여러 레벨의 많은 컴포넌트에 주어야 할 때 라면, context API 혹은 Redux를 사용해 주는 것이 좋다.  

 

단순히 prop drilling을 피하기 위함이라면 context API를 사용하면 좋고, 

더 복잡한 큰 규모의 프로젝트나 사이드이펙트를 줄이기 위한 강력한 기능이 필요하다면 Redux + React-Redux 를 사용하면 좋다.

 

 

 

https://ko.reactjs.org/docs/context.html

https://hyongti.tistory.com/m/43