프론트엔드/상태 관리

리액트 Context로 props drilling 해결하기

Junheehee 2022. 10. 27. 17:23

리액트는 props를 통해 데이터를 상위 컴포넌트에서 하위 컴포넌트로 전달한다.

리액트 어플리케이션을 개발하다보면 여러 겹의 컴포넌트를 거쳐 데이터를 전달하는 일이 빈번하게 생긴다.

 

 

 

 

Props Drilling

 

간단한 예시를 만들어 봤다.

import { useState } from "react";
import "./App.css";

function App() {
  const [cnt, setCnt] = useState(0);
  return (
    <div className="App">
      <button
      	className="button"
        onClick={() => setCnt(cnt + 1)}
      >
        +1
      </button>
      <RedBox />
    </div>
  );
}

function RedBox() {
  return (
    <div className="red-box">
      <GreenBox />
    </div>
  );
}

function GreenBox() {
  return (
    <div className="green-box">
      <YellowBox />
    </div>
  );
}
function YellowBox() {
  return <div className="yellow-box"></div>;
}

export default App;

 

App의 cnt를 YellowBox에서 사용하기 위해선 App -> RedBox -> GreenBox -> YellowBox 를 거쳐서 데이터를 보내줘야 한다.

 

 

 

 

import { useState } from "react";
import "./App.css";

function App() {
  const [cnt, setCnt] = useState(0);
  return (
    <div className="App">
      App
      <button
      	className="button"
        onClick={() => setCnt(cnt + 1)}
      >
        +1
      </button>
      <RedBox cnt={cnt} />
    </div>
  );
}

function RedBox({ cnt }) {
  return (
    <div className="red-box">
      <GreenBox cnt={cnt} />
    </div>
  );
}

function GreenBox({ cnt }) {
  return (
    <div className="green-box">
      <YellowBox cnt={cnt} />
    </div>
  );
}
function YellowBox({ cnt }) {
  return <div className="yellow-box">{cnt}</div>;
}

export default App;

props drilling

 

App과 YellowBox 사이에 있는 RedBox와 GreenBox는 props로 전달 받은 cnt를 실제로 사용하지 않는다.

이 중간 컴포넌트들은 단지 하위 컴포넌트로 다시 전달하기 위해서 props를 통해 데이터를 받고 있다.

이를 props drilling이라 한다.

 

 

만약 YellowBox가 App의 데이터를 더 받거나 혹은 받지 않는다면, 중간의 컴포넌트들도 그에 맞춰 수정해줘야 한다.

중간의 컴포넌트들은 사용하지 않는 props 때문에 코드를 이해하기 어렵고, 재사용도 불편하다.

 

 

이 때, 리액트의 Context를 이용하면 props drilling을 해결할 수 있다.

 

 

 

 

Context

 

 

Context – React

A JavaScript library for building user interfaces

reactjs.org

context는 데이터를 전역에서 접근할 수 있게 해준다.

데이터를 전역에서 접근할 수 있기 때문에 컴포넌트마다 일일이 props를 넘겨주지 않아도 된다.

 

 

import { useState, createContext, useContext } from "react";
import "./App.css";

const CntContext = createContext("No!");

function App() {
  const [cnt, setCnt] = useState(0);
  return (
    <div className="App">
      App
      <button
      	className="button"
        onClick={() => setCnt(cnt + 1)}
      >
        +1
      </button>
      <CntContext.Provider value={cnt}>
        <RedBox />
      </CntContext.Provider>
    </div>
  );
}

function RedBox() {
  return (
    <div className="red-box">
      <GreenBox />
    </div>
  );
}

function GreenBox() {
  return (
    <div className="green-box">
      <YellowBox />
    </div>
  );
}
function YellowBox() {
  const cnt = useContext(CntContext);
  return <div className="yellow-box">{cnt}</div>;
}

export default App;

 

context 오브젝트는 리액트의 createContext로 생성된다.

context 오브젝트의 Provider 컴포넌트는 context가 변화할 때마다 하위의 컴포넌트에게 context의 value를 전달한다.

그리고 리액트의 useContext를 이용하면 value에 접근할 수 있다.

 

 

위에선 createContext로 CntContext를 만들었다.

CntContext의 Provider 컴포넌트로 App의 cnt를 전역으로 접근할 수 있게 만들었고,

YellowBox에서 useContext로 cnt를 가져왔다.

이제 중간의 컴포넌트들, RedBox와 GreenBox는 cnt를 props로 전달받지 않아도 되니 코드가 한결 보기 좋아졌다.

 

 

useContext를 통해 context에 접근할 때, 상위의 컴포넌트를 하나씩 올라가며 해당하는 Provider을 찾는다.

만약 끝까지 올라가도 Provider을 찾지 못한다면, createContext의 인수인 default value를 반환한다.

import { useState, createContext, useContext } from "react";
import "./App.css";

const CntContext = createContext("No!");

function App() {
  const [cnt, setCnt] = useState(0);
  return (
    <div className="App">
      App
      <button
      	className="button"
        onClick={() => setCnt(cnt + 1)}
      >
        +1
      </button>
      <CntContext.Provider value={cnt}>
      </CntContext.Provider>
      <RedBox />
    </div>
  );
}

function RedBox() {
  return (
    <div className="red-box">
      <GreenBox />
    </div>
  );
}

function GreenBox() {
  return (
    <div className="green-box">
      <YellowBox />
    </div>
  );
}
function YellowBox() {
  const cnt = useContext(CntContext);
  return <div className="yellow-box">{cnt}</div>;
}

export default App;

상위에 Provider가 없을 때

 

코드를 보면 RedBox 컴포넌트가 Provider 밖에 나와있다.

따라서 YellowBox에서 상위로 끝까지 올라가도 CntContext.Provider을 찾을 수 없고, default value인 No!가 렌더링 되었다.

 

 

context는 데이터를 전역으로 접근할 수 있게 해주는 반면, 데이터를 변경하는 기능은 따로 제공하지 않는다.

데이터를 변경하려면 context value에 변경 함수를 넣어주면 된다.

 

 

 

 

Context의 단점

 

context를 사용하면, Provider의 모든 하위 컴포넌트들은 context의 value가 변경될 때마다 리렌더링된다.

불필요한 상황에도 컴포넌트가 리렌더링되면 어플리케이션 성능에 악영향을 끼칠 수 있다.

 

Provider 하위 컴포넌트 리렌더링

 

 

 

 

하위 컴포넌트의 불필요한 리렌더링을 방지하는 몇가지 방법이 있다.

 

첫번째는 React.memo로 메모이제이션하는 것이다.

import React, { useState, createContext, useContext, useEffect } from "react";
import "./App.css";

const CntContext = createContext("No!");

function App() {
  const [cnt, setCnt] = useState(0);
  return (
    <div className="App">
      App
      <button
      	className="button"
        onClick={() => setCnt(cnt + 1)}
      >
        +1
      </button>
      <CntContext.Provider value={cnt}>
        <RedBox />
      </CntContext.Provider>
    </div>
  );
}

const RedBox = React.memo(function RedBox() {
  useEffect(() => {
    console.log("render in red");
  });
  return (
    <div className="red-box">
      <GreenBox />
    </div>
  );
});

function GreenBox() {
  useEffect(() => {
    console.log("render in green");
  });
  return (
    <div className="green-box">
      <YellowBox />
    </div>
  );
}
function YellowBox() {
  const cnt = useContext(CntContext);
  return <div className="yellow-box">{cnt}</div>;
}

export default App;

React.Memo로 리렌더링 방지

 

 

 

 

redux 같은 상태 관리 라이브러리를 이용하는 방법도 있다.

redux는 context와 마찬가지로 데이터를 전역에서 접근할 수 있게 해주지만 차이점이 많다.

따라서 context든 redux든 목적과 상황에 맞게 사용해야 한다.

나는 여러 컴포넌트에서 사용하면서도 자주 변경되지 않는 데이터를 관리할 때 context를 사용한다.