[React] 다시 한번 useContext를 파헤쳐보자

sohyeon kim·2022년 8월 2일
5

React & Javascript

목록 보기
37/41

useContext 훅은 어떻게 사용할까?

리액트의 일반적인 데이터 흐름은 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 '단방향'으로 흐른다. 엄청 큰 컴포넌트 트리가 있다고 가정할 때 공통적으로 필요한 전역적인 데이터가 있을 수 있다. 예를 들어 userdata
전역 데이터를 일일이 props로 단계별로 전달해야 한다면 정말 비효율 적이다.
리액트는 이러한 문제점을 해결해 주는 Context API를 제공한다.

Context는 앱 안에서 전역적으로 사용되는 데이터를 여러 컴포넌트끼리 쉽게 공유할 수 있는 방법을 제공한다. Context를 사용하면 Props로 데이터를 일일이 전달해 주지 않아도 해당 데이터를 가지고 있는 상 위 컴포넌트에 그 데이터가 필요한 하위 컴포넌트가 접근할 수 있다. 즉 사용자 정보, 테마, 언어 등 전역적인 데이터를 전달하기에 정말 편리하다.

상위 컴포넌트의 data가 필요한 하위 컴포넌트들은 useContext 훅을 사용해서 해당 데이터를 받아오기만 하면 된다.
useContext는 Context로 분류한 데이터를 쉽게 받아올 수 있게 도와주는 역할을 한다.


Q. context가 이렇게 좋은데 그럼 굳이 왜 props를 사용하는가?

context는 꼭 필요할 때만 사용한다. context를 사용하면 컴포넌트를 재사용하기 어려워질 수 있다. context의 주된 목적인 다양한 레벨이 있는 많은 컴포넌트들에게 전역적인 데이터를 전달하기 위함이다. 리액트 공식 문서에는 context를 사용하는 이유가 단지 prop drilling을 핗기 위한 목적이라면 component composition(컴포넌트 합성)이 더 간단한 해결책이라고 제시하고 있다.


그럼 예제를 통해 useContext를 파헤쳐보자.
먼저 context를 사용하지 않고, state와 props만 사용해서 만든 웹사이트다.
header, content, footer 로 구성되어있고, footer에 있는 버튼을 누르면 다크모드로 화면이 처리가 된다.

👑 최상위 컴포넌트

// 최상위 컴포넌트
import { useState } from "react";
import "./App.css";
import Page from "./Compopnents/Page";


function App() {
  // 햔제 App 이 다크모드인지 아닌지 true false 로 정보를 받고 있다. 
  const [isDark, setIsDark] = useState(false);
  
  // Page 자식 컴포넌트에게 해당 데이터를 props로 넘겨 주고 있다. 
  return <Page isDark={isDark} setIsDark={setIsDark}/>
}
export default App;

🐓 Page 컴포넌트

// page 컴포넌트
import React, { useContext } from "react";
import Content from "./Context";
import Header from "./Header";
import Footer from "./Footer";

const Page = ({isDark, setIsDark}) => {
  return (
    <div className="page">
      <Header isDark={isDark}/>
      <Content isDark={isDark}/>
      <Footer isDark={isDark} setIsDark={setIsDark}/>
    </div>
  );
};

export default Page;
  • 전체화면을 담고 있는 컴포넌트

  • 3개의 자식 컴포넌트를 가지고 있다.

  • {isDark, setIsDark} 를 props로 받아오고 있다.

  • isDark 를 모든 자식 컴포넌트에게 전달을 해주고, setIsDark는 Footer에게 전달해준다.


🐥 Header 컴포넌트

import React from "react";

const Header = ({ isDark }) => {
  return (
    <header
      className="header"
      style={{
        backgroundColor: isDark ? "black" : "lightgray",
        color: isDark ? "white" : "black",
      }}
    >
      <h1>Welcome 홍길동</h1>
    </header>
  );
};

export default Header;
  • isDark 를 props로 전달받고 있다.

  • header 태그는 isDark가 true 이면 배경이 black, false면 lightgray 아래 글씨 색상도 동일하게 작동한다.


🐥 Content 컴포넌트

import React from "react";

const Content = ({ isDark }) => {
  return (
    <div
      className="content"
      style={{
        backgroundColor: isDark ? "black" : "white",
        color: isDark ? "white" : "black",
      }}
    >
      <p>홍길동님, 좋은 하루 되세요 </p>
    </div>
  );
};

export default Content;
  • isDark 를 props로 전달받고 있다.
  • header 와 마찬가지고 isDark 가 true냐 false이냐에 따라 배경과 글자색상이 달라진다.

import React from "react";

const Footer = ({ isDark, setIsDark }) => {
  const toggleTheme = () => {
    setIsDark(!isDark);
  };
  return (
    <footer
      className="footer"
      style={{ backgroundColor: isDark ? "black" : "lightgray" }}
    >
      <button className="button" onClick={toggleTheme}>
        Dark Mode
      </button>
    </footer>
  );
};

export default Footer;
  • isDark와 setIsDark를 props로 전달받고 있다.

  • isDark 가 true냐 false이냐에 따라 배경과 글자색상이 달라진다.

  • button 이 클릭될 때마다 toggleTheme 함수가 실행된다.

  • toggleTheme 는 현재 isDark가 true 면 false로 바꿔주고 false 면 true 로 바꿔준다.


여기서 App 컴포넌트가 가지고 있는 isDark 는 전체적인 테마에 관련된 data를 담고 있기 때문에 전역적이다. page 컴포넌트에게 isDark를 넘겨준다.


하지만 page 컴포넌트는 isDark를 받아오고 있지만 실질적으로 사용하지 않는다. 단지 자식 컴포넌트에게 전달만 해주고 있다. 그렇기 때문에 page 컴포넌트는 isDark 가 필요하지 않는 중간 컴포넌트라고 할 수 있다.


자 그럼 이제 App 컴포넌트가 가지고 있는 isDark 데이터를 모든 하위 컴포넌트에게 props를 사용하지 않고 context를 사용해서 공유해보자.

그럼 이제 page 컴포넌트는 isDark 데이터에 대해 전혀 몰라도 된다.

먼저 context를 만들어줄 폴더하나를 만들어준다.

react context를 만들려면 가장 먼저 createContext를 import 시켜줘야한다.

import { createContext } from "react";

// 기본값으로는 null을 넣어준다.
export const ThemeContext = createContext(null);

App 컴포넌트로 돌아가서 위 context를 import 시켜준다.

  • 그리고 page 컴포넌트를 만들어준 context의 provider로 감싸준다.

  • context의 provider는 value 라는 props를 받는데 이 안에는 전달하고자 하는 데이터를 넣어준다.

  • ThemeContext 감싸는 모든 하위 컴포넌트는 props를 사용하지 않고 value로 넣어준 값에 접근할 수 있다. 그리고 페이지 컴포넌트가 갖는 값들을 지워준다.

import { useState } from "react";
import "./App.css";
import Page from "./Compopnents/Page";
import { ThemeContext } from "./context/ThemeContext";

function App() {
  const [isDark, setIsDark] = useState(false);

  return (
    // 📌
    <ThemeContext.Provider value={{ isDark, setIsDark }}>
      <Page />
    </ThemeContext.Provider>
  );
}
export default App;

page 컴포넌트에서 인자로 받아오는 {isDark, setIsDark} 를 지워준다. 그리고 중간 컴포넌트이니 isDark와 가 필요가 없다.

import React from "react";
import Content from "./Context";
import Header from "./Header";
import Footer from "./Footer";

// isDark 를 실질적으로 사용하지 않고, 자녀 컴포넌트들에게 전달하는 역할
// data 필요하지 않음 !
const Page = () => {
  return (
    <div className="page">
      <Header/>
      <Content/>
      <Footer/>
    </div>
  );
};

export default Page;

header에서는 props를 지워주고 useContext 훅을 사용해 ThemeContext를 불러온다.

import { useContext } from "react";
 // 📌 
import { ThemeContext } from "../context/ThemeContext";

const Header = () => {
  // 📌 
  const { isDark } = useContext(ThemeContext);
  return (
    <header
      className="header"
      style={{
        backgroundColor: isDark ? "black" : "lightgray",
        color: isDark ? "white" : "black",
      }}
    >
      <h1>Welcome 홍길동!</h1>
    </header>
  );
};

export default Header;

content 컴포넌트도 바꿔준다.

import React, { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";

const Content = () => {
    // 📌 
  const { isDark } = useContext(ThemeContext);

  return (
    <div
      className="content"
      style={{
        backgroundColor: isDark ? "black" : "white",
        color: isDark ? "white" : "black",
      }}
    >
      <p>홍길동님, 좋은 하루 되세요 </p>
    </div>
  );
};

export default Content;

마지막 footer 컴포넌트

import React, { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";

const Footer = () => {
  // 📌
  const { isDark, setIsDark } = useContext(ThemeContext);
  const toggleTheme = () => {
    setIsDark(!isDark);
  };
  return (
    <footer
      className="footer"
      style={{ backgroundColor: isDark ? "black" : "lightgray" }}
    >
      <button className="button" onClick={toggleTheme}>
        Dark Mode
      </button>
    </footer>
  );
};

export default Footer;

자, context를 활용해도 웹사이트는 정상적으로 작동하는 것을 확인할 수 있다.

지금까지를 정리해보면 App 컴포넌트에서 context를 불러와서 모든 하위 컴포넌트에게 알린 것이라고 할 수 있다. page 컴포넌트는 해당 데이터가 필요하지 않으니까 context를 쓸 필요가 없고, page의 자식 컴포넌트는 해당 데이턱 필요하니 useContext 훅을 사용해서 해당 전역 데이터에 접근한 것이다.


그럼 직접 만든 context로 돌아가서 createContext에 인자로 받아온 초기값에 대해서 알아보자.

초기값이 null 이 아닌 hello로 바꾸고,

App 컴포넌트에 들어가서 provider를 없애고, page 컴포넌트에서 우선 header 와 content 그리고 footer를 주석 처리하고 useContext를 사용해서 ThemeContext 값을 받아오자.

console에 찍어보면 초기값 hello가 찍힌 것을 확인할 수 있다.

Q. 이게 어떻게 된 것인가?

만약에 useContext로 ThemeContext의 정보를 받아왔는데 상위에서 해당 context에 provider로 감싸주지 않았다면 즉 value 가 없으니 이 경우에는 ThemeContext에서 인자로 넘겨준 초기값을 받아오게 되는 것이다.

하지만 우리가 만든 App은 value 를 사용해서 값을 넘겨주기 때문에 초기값이 필요하지 않아 null로 값을 준다.


user 정보 건네주기

사용자에 대한 정보를 context로 넘겨줘보자. 홍길동을 사용자로 바꿔보기 !

// 사용자 정보

import { createContext } from "react";

export const UserContext = createContext(null);

App 컴포넌트에서 UserContext import 시켜주고 가장 상위에서 provider로 감싸주자. value는 {사용자}

import { useState } from "react";
import "./App.css";
import Page from "./Compopnents/Page";
import { ThemeContext } from "./context/ThemeContext";
// 📌
import { UserContext } from "./context/UserContext";

function App() {
  const [isDark, setIsDark] = useState(false);
  return (
    // 📌
    <UserContext.Provider value={"사용자"}>
      <ThemeContext.Provider value={{ isDark, setIsDark }}>
        <Page />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}
export default App;

header 에서 설정해주기

import { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
import { UserContext } from "../context/UserContext";

const Header = () => {
  const { isDark } = useContext(ThemeContext);
  // 📌
  const user = useContext(UserContext);
  // console.log(user);
  return (
    <header
      className="header"
      style={{
        backgroundColor: isDark ? "black" : "lightgray",
        color: isDark ? "white" : "black",
      }}
    >
      <h1>Welcome {user}</h1>
    </header>
  );
};

export default Header;

content 에서 설정해주기

import React, { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
import { UserContext } from "../context/UserContext";

const Content = () => {
  const { isDark } = useContext(ThemeContext);
  // 📌
  const user = useContext(UserContext);

  return (
    <div
      className="content"
      style={{
        backgroundColor: isDark ? "black" : "white",
        color: isDark ? "white" : "black",
      }}
    >
      <p>{user}, 좋은 하루 되세요 </p>
    </div>
  );
};

export default Content;

해당 결과가 잘 반영된 것을 볼 수 있다

주로 수업과 프로젝트에서는 상태 관리로 가장 최근에 출시된 라이브러리인 recoil를 사용했기 때문에 useContext에 대한 지식은 그냥 스쳐 지나가는 정도였다. recoil은 매우 간단하고 간단하다. atom과 selector라는 개념을 사용한다. 이번 계기로 ContextAPI에 대한 기본 개념에 대해 익혀봤고, 이것을 지금 당장 사용할지는 모르겠지만 recoil과 비교하면서 더 깊이 해보면 좋을 것 같다.



참고
리액트 공식 홈페이지
별코딩-리액트훅스시리즈

profile
slow but sure

1개의 댓글

comment-user-thumbnail
2024년 2월 22일

감사합니다 덕분에 이해가 잘 됐어요

답글 달기