덕구공 2022. 6. 6. 13:22

공식문서

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

Context API?

  • react는 16.3 버전부터 정식적으로 Context API를 지원하고 있다. 일반적으로 부모와 자식간 props를 날려 state를 변화시키는 것과는 달리 context api는 컴포넌트 간 간격이 없다.
    • context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.
  • 즉, 컴포넌트를 건너띄고 다른 컴포넌트에서 state, function을 사용할 수 있다. 또한 redux의 많은 어려운 개념보다 context API는 Provider, Consumer, createContext 개념만 알면 적용이 가능하다
  • 변경이 잦게 일어나는 전역 데이터를 사용할 때는 사실 그다지 적합하진 않다. 작은 데이터 조각을 여기저기서 사용하고 싶을 땐 제법 효율이 좋다.

사용법

  • 데이터를 저장할 공간을 만들어서 가져다 쓰기 위해 Provider로 데이터를 사용할 전역 컴포넌트(App.js나 index.js)를 감싸서 데이터를 주입하고 Provider에서 가지고 있는 전체 데이터를 Consumer(주입한 데이터를 구독하게 하는 역할)로 한번 더 감싼 다음 그 안에 컴포넌트를 넣는다.
  • Consumer 없이 useContext라는 훅으로 데이터를 구독할 수 있다.
  • 대략적인 구조는 아래와 같다
<Provider>
  <Consumer>
    <Component1/>
    <Component2/>
  </Consumer>
</Provider>

1. 저장소 만들기 createContext()

  • createContext()로 contextAPI를 생성한다.
  • createContext()인자로 default값을 넣을 수 있는데 만약 Consumer가 Provider로 감싸져 있지 않다면 해당 디폴트 값을 쓸 수 있다.
import React from "react";
// 자식이 context 하기 위해 export
export const store = React.createContext();
const App = () => {
  return (

  )
}

export default App;

2. 데이터 주입하기

  • <store이름.Provider value={전역 상태}>로 데이터를 주입!
  • 반드시 value를 꼭 명시해야 한다!
import React from "react";

export const store = React.createContext();
const App = () => {
  return (
    <store.Provider value={{name:"duck", height: 180}}>

    </store.Provider>
  )
}

export default App;

3-1. 데이터 구독하기

  • 데이터를 구독할 컴포넌트에서 store를 불러와서 로 감싼 후 { }안에 함수를 작성해서 Provider에서 보낸 value를 인자로 넣어서 사용할 수 있다
App.js
import React from "react";
import Component from "./Component";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");
const App = () => {
  return (
    <store.Provider value={{name:"duck", height: 180}}>
      <Component></Component>
    </store.Provider>
  )
}

export default App;
Component.js
// context를 가져옴
import { store } from "./App";
const Component = () =>  {
    return(
        <store.Consumer>
            {(value) => (<div>{value.name}</div>)}
        </store.Consumer>
    );
}
export default Component;

주의사항 - Provider가 없는 Consumer

  • 만약, Provider로 감싸지지 않은 컴포넌트가 Consumer로 store를 구독하면 Provider에서 넘겨준 디폴트 값이 넘어간다!!
App.js
import React from "react";
import Component from "./Component";

export const store = React.createContext("default");
const App = () => {
  return (
      <Component></Component>
  )
}

export default App;

Component.js

// context를 가져옴
import { store } from "./App";
const Component = () =>  {
    return(
        <store.Consumer>
            {(value) => (<div>{value}</div>)}
        </store.Consumer>
    );
}
export default Component;

3-2. 데이터 구독하기 useContext()

  • useContext() 훅을 이용해서 store를 구독할 수 있다!

App.js

import React from "react";
import Component from "./Component";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");
const App = () => {
  return (
    <store.Provider value={{name:"duck", height: 180}}>
      <Component></Component>
    </store.Provider>
  )
}

export default App;
Component.js
// context를 가져옴
import { store } from "./App";
import { useContext } from "react";
const Component = () =>  {
    // useContext로 store의 데이터 구독!
    const data = useContext(store);
    console.log(data)
    return(
        <>
            <div>name: {data.name}</div>
            <div>height: {data.height}</div>
        </> 
    );
}
export default Component;

주의사항 - Provider가 없는 Consumer

  • 만약, Provider로 감싸지지 않은 컴포넌트가 Consumer로 store를 구독하면 Provider에서 넘겨준 디폴트 값이 넘어간다!!
App.js
import React from "react";
import Component from "./Component";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");
const App = () => {
  return (
      <Component></Component>
  )
}

export default App;
Component.js
// context를 가져옴
import { store } from "./App";
import { useContext } from "react";
const Component = () =>  {
    const data = useContext(store);
    console.log(data)
    return(
        <>
            <div>{data}</div>
        </> 
    );
}
export default Component;

4. 데이터 변경하기 - useState() 사용

  • Provider에 단순히 데이터 뿐만 아니라 함수도 넘길 수 있다!
  • App.js에서 useState를 이용해서 state와 state를 변경하는 함수를 Provider의 value에 넘겨서 자식 컴포넌트들이 구독해서 데이터를 변경할 수 있다!
  • 아래 예시는 App.js의 state와 state 변경 함수를 Provider의 value에 넘겨서 자식 컴포넌트가 state와 state 변경 함수를 받아와서 App.js의 state 값을 변경시키는 예시이다!
App.js
import React from "react";
import { useState } from "react";
import Component from "./Component";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");
const App = () => {
  // Provider의 value로 넘겨줄 state와 state 변경 함수
  const [data, setData] = useState({name:"duck", height: 180})
  return (
    <store.Provider value={[data, setData]}>
      <Component></Component>
    </store.Provider>
  )
}
export default App;
Component.js
// context를 가져옴
import { store } from "./App";
import { useContext } from "react";
const Component = () =>  {
    // Provider에서 받은 App.js의 state와 state변경 함수
    const [data, setData] = useContext(store);
    const add_height = () => {
        // state의 height 값이 1씩 증가!
        setData({...data, height: data.height+1})
    }
    console.log(data)
    return(
        <>
            <div>name: {data.name}</div>
            <div>name: {data.height}</div>
            <button onClick={add_height}>키 늘리기</button>
        </> 
    );
}
export default Component;

렌더링 이슈

  • Provider의 value 값이 변하면 Provider 하위의 context를 구독하지 않은 자식들까지 렌더링이 발생한다!!
  • 아래 예시를 살펴보자.
  • GrandChildComponent는 useContext를 사용해서 Proivder의 value가 변경이 되면 리렌더링이 일어나야겠지만 ChildComponent는 context를 구독하지 않고 있기 때문에 리렌더링이 필요하지 않다!
import React from "react";
import { useState, useContext } from "react";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");

const Component = () =>  {
  // Provider에서 받은 App.js의 state와 state변경 함수
  const [data, setData] = useContext(store);
  const add_height = () => {
      // state의 height 값이 1씩 증가!
      setData({...data, height: data.height+1})
  }
  return(
      <>
          <div>name: {data.name}</div>
          <div>name: {data.height}</div>
          <button onClick={add_height}>키 늘리기</button>
          <ChildComponent/>
      </> 
  );
}

// context를 구독하지 않는 자식이 리렌더링 발생
const ChildComponent = (props) => {
  console.log("ChildComponent")
  return(<GrandChildComponent/>)
}

// memo로 부모가 리렌더링이 방지되어 있지만 useContext로 store를 구독하기 때문에
// 렌더링이 일어남!
const GrandChildComponent = () => {
  const [data, setData] = useContext(store)
  console.log("GrandChildComponent")
  return(null)
}

const App = () => {
  // Provider의 value로 넘겨줄 state와 state 변경 함수
  const [data, setData] = useState({name:"duck", height: 180})
  return (
    <store.Provider value={[data, setData]}>
      <Component></Component>
    </store.Provider>
  )
}
export default App;
  • Provider 하위에 있지만, context를 사용하지 않는 ChildComponent까지 리렌더링이 일어난다!

  • 이러한 불필요한 리렌더링을 방지하기 위해 Context를 구독하지 않는 자식들은 React.memo를 사용해서 리렌더링을 방지하자!!
import React from "react";
import { useState, useContext } from "react";
// store를 export해서 자식에서 사용할 수 있게 한다!
export const store = React.createContext("default");

const Component = () =>  {
  // Provider에서 받은 App.js의 state와 state변경 함수
  const [data, setData] = useContext(store);
  const add_height = () => {
      // state의 height 값이 1씩 증가!
      setData({...data, height: data.height+1})
  }
  return(
      <>
          <div>name: {data.name}</div>
          <div>name: {data.height}</div>
          <button onClick={add_height}>키 늘리기</button>
          <ChildComponent/>
      </> 
  );
}

// context를 사용하지 않는 컴포넌트에 memo를 사용해서 리렌더링 방지!
const ChildComponent = React.memo((props) => {
  console.log("ChildComponent")
  return(<GrandChildComponent/>)
})

// memo로 부모가 리렌더링이 방지되어 있지만 useContext로 store를 구독하기 때문에
// 리렌더링이 일어남
const GrandChildComponent = () => {
  const [data, setData] = useContext(store)
  console.log("GrandChildComponent")
  return(null)
}

const App = () => {
  // Provider의 value로 넘겨줄 state와 state 변경 함수
  const [data, setData] = useState({name:"duck", height: 180})
  return (
    <store.Provider value={[data, setData]}>
      <Component></Component>
    </store.Provider>
  )
}
export default App;