React Context API 기초 & 심화

김명원·2024년 12월 21일
0

learnReact

목록 보기
6/26

🎨 React Context를 사용해 데이터를 효율적으로 전달하기

React에서 부모 컴포넌트가 자식 컴포넌트로 데이터를 전달할 때 props를 사용합니다.
하지만 중간에 여러 컴포넌트를 거치는 Prop Drilling 상황이 발생하면 관리가 어렵고 불편합니다.

이를 해결하기 위해 React Context API를 활용하여 데이터를 트리 전체에 손쉽게 전달하는 방법을 살펴봅니다.


💡 Prop Drilling 문제란?

Prop Drilling은 데이터가 필요한 컴포넌트에 도달하기 위해, 불필요하게 중간 컴포넌트들을 거쳐야 하는 상황을 말합니다.

문제 상황

  • Header, Main, Footer 컴포넌트 모두 부모 컴포넌트로부터 동일한 데이터를 받아야 합니다.
  • 이 데이터는 다크 모드 전환 상태(darkMode)입니다.

기존 구조


🛠️ Step 1: Prop을 사용해 데이터 전달하기

import "./AppTheme.css";
import HeaderTheme from "./components/theme/Header.jsx";
import MainTheme from "./components/theme/Main.jsx";
import FooterTheme from "./components/theme/Footer.jsx";
import { useState } from "react";

function AppTheme() {
  const [darkMode, setDarkMode] = useState(false);

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  return (
    <>
      <HeaderTheme darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
      <MainTheme darkMode={darkMode} />
      <FooterTheme darkMode={darkMode} />
    </>
  );
}

export default AppTheme;

각 컴포넌트에 darkModetoggleDarkMode를 전달합니다.


HeaderTheme.jsx

export default function Header({ darkMode, toggleDarkMode }) {
  return (
    <header className={`header ${darkMode ? "header--dark" : "header--light"}`}>
      <h1 className="header__title">헤더 컴포넌트</h1>
      <button onClick={toggleDarkMode} className="header__button">
        {darkMode ? "라이트 모드로 전환" : "다크 모드로 전환"}
      </button>
    </header>
  );
}

FooterTheme.jsx

export default function Footer({ darkMode }) {
  return (
    <footer className={`footer ${darkMode ? "footer--dark" : "footer--light"}`}>
      <p className="footer__text">푸터 컴포넌트</p>
    </footer>
  );
}

문제점

  • 데이터가 필요한 컴포넌트가 많을수록 props 전달이 비효율적입니다.
  • 중간 컴포넌트가 데이터 전달만 담당하면 코드가 장황해집니다.

🛠️ Step 2: Context API로 데이터 전달하기

🔑 Context API란?

Context는 부모 컴포넌트가 트리 전체에 데이터를 전달할 수 있도록 하는 React의 내장 기능입니다.
이를 사용하면 데이터가 필요한 컴포넌트에 "순간 이동"하듯 값을 전달할 수 있습니다.


1️⃣ Context 생성하기

src/context/DarkModeContext.jsx

import { createContext } from "react";

export const DarkModeContext = createContext(false);

DarkModeContext를 생성합니다. 이를 통해 데이터를 공유할 수 있습니다.


2️⃣ Provider를 사용해 데이터 전달하기

AppTheme.jsx

import { useState } from "react";
import { DarkModeContext } from "./context/DarkModeContext";
import HeaderTheme from "./components/theme/Header";
import MainTheme from "./components/theme/Main";
import FooterTheme from "./components/theme/Footer";

function AppTheme() {
  const [darkMode, setDarkMode] = useState(false);

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  return (
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      <HeaderTheme />
      <MainTheme />
      <FooterTheme />
    </DarkModeContext.Provider>
  );
}

export default AppTheme;
  • DarkModeContext.Provider를 사용해 darkModetoggleDarkMode를 하위 컴포넌트로 전달합니다.
  • 이제 중간 컴포넌트를 거치지 않아도 데이터를 사용할 수 있습니다.

3️⃣ useContext로 데이터 사용하기

Header.jsx

import { useContext } from "react";
import { DarkModeContext } from "../../context/DarkModeContext";

export default function Header() {
  const { darkMode, toggleDarkMode } = useContext(DarkModeContext);

  return (
    <header className={`header ${darkMode ? "header--dark" : "header--light"}`}>
      <h1 className="header__title">헤더 컴포넌트</h1>
      <button onClick={toggleDarkMode} className="header__button">
        {darkMode ? "라이트 모드로 전환" : "다크 모드로 전환"}
      </button>
    </header>
  );
}

Footer.jsx

import { useContext } from "react";
import { DarkModeContext } from "../../context/DarkModeContext";

export default function Footer() {
  const { darkMode } = useContext(DarkModeContext);

  return (
    <footer className={`footer ${darkMode ? "footer--dark" : "footer--light"}`}>
      <p className="footer__text">푸터 컴포넌트</p>
    </footer>
  );
}

Card.jsx

import { useContext } from "react";
import { DarkModeContext } from "../context/DarkModeContext";

export default function Card({ title, children }) {
  const { darkMode } = useContext(DarkModeContext);

  return (
    <div className={`card ${darkMode ? "card--dark" : "card--light"}`}>
      <div className="card__header">{title}</div>
      <div className="card__body">{children}</div>
    </div>
  );
}

🎉 결과

  • 모든 컴포넌트가 props 없이 darkModetoggleDarkMode를 사용할 수 있습니다.
  • Header, Footer, Card가 필요할 때 useContext를 통해 데이터에 접근합니다.

🚀 요약

  1. Prop Drilling 문제: 중간 컴포넌트를 거쳐 데이터를 전달하면 관리가 복잡해집니다.
  2. Context API: 데이터를 트리 전체에 쉽게 전달할 수 있는 강력한 도구입니다.
  3. Provider와 Consumer:
    • Provider는 데이터를 전달.
    • useContext는 데이터를 사용.
  4. 코드 간소화:
    • 중간 컴포넌트가 데이터 전달 역할을 하지 않아도 됩니다.
  5. 확장 가능성:
    • Context를 사용하면 애플리케이션 전역 상태 관리에도 유용합니다.

🌟 Context API 더 깊게 이해하기

React의 Context API는 데이터를 컴포넌트 트리 전체에 "순간 이동"시킬 수 있는 강력한 도구입니다.
특히, 여러 컴포넌트가 공통 데이터를 공유해야 할 때 매우 유용합니다.


🛠️ 코드 구현

1️⃣ DarkModeContext.jsx

컨텍스트를 정의하고, DarkModeProvider를 통해 상태와 동작을 제공합니다.

import { createContext, useState } from "react";

export const DarkModeContext = createContext();

export const DarkModeProvider = ({ children, initDarkMode = true }) => {
  const [darkMode, setDarkMode] = useState(initDarkMode);

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  return (
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
};

2️⃣ AppTheme.jsx

DarkModeProvider를 사용해 Context를 제공하며, 초기 값을 설정합니다.

import "./AppTheme.css";
import HeaderTheme from "./components/theme/Header.jsx";
import MainTheme from "./components/theme/Main.jsx";
import FooterTheme from "./components/theme/Footer.jsx";
import { DarkModeProvider } from "./context/DarkModeContext.jsx";

function AppTheme(props) {
  return (
    <DarkModeProvider initDarkMode={false}>
      <HeaderTheme />
      <MainTheme />
      <FooterTheme />
    </DarkModeProvider>
  );
}

export default AppTheme;

3️⃣ Main.jsx

DarkModeProvider를 중첩하여 특정 섹션에만 별도의 Dark Mode 컨텍스트를 제공합니다.

import { DarkModeProvider } from "../../context/DarkModeContext";
import Card from "../Card";

export default function Main() {
  return (
    <main>
      <DarkModeProvider>
        <Card title="제목">
          Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus iste laborum iusto ipsa cum est dolorum pariatur eaque ipsam at, sunt exercitationem dolor magnam, et esse. Porro
          laborum eligendi nemo.
        </Card>
      </DarkModeProvider>
    </main>
  );
}

🖼️ 현재 컴포넌트 구조

이 상태에서는 DarkModeProvider를 중첩하여 다른 상태를 관리할 수도 있습니다.


✨ Context API 사용 예시

✔️ 대표적인 사용 사례

  • 테마 지정하기
  • 현재 로그인 계정 관리
  • 라우팅 정보 공유
  • 상태 관리 (State Management)

💡 팁

1️⃣ 자주 변경되지 않는 데이터를 Context로 사용
Context 상태가 변경되면 하위 모든 컴포넌트가 리렌더링되므로, 빈번히 업데이트되는 데이터는 Context 사용에 적합하지 않습니다.

2️⃣ 필요한 범위에서만 Context 제공
꼭 필요한 섹션에서만 Provider를 사용하는 것이 성능과 유지보수에 유리합니다.


🚀 최종 요약

Context는 Prop Drilling 문제를 해결하고, 여러 컴포넌트가 공통 상태를 공유할 수 있는 강력한 도구입니다.
그러나, 사용 목적과 데이터 변경 빈도를 고려하여 적절히 활용하는 것이 중요합니다!


🛠️ Reducer와 Context로 앱 확장하기 - 심화

Reducer와 Context를 사용하면 앱 상태 관리를 더 간결하고 효율적으로 할 수 있습니다.
이 문서에서는 할 일 관리 앱을 예제로, ReducerContext를 결합하여 앱을 확장하는 방법을 심층적으로 다룹니다.


🌟 앱 구조

ReducerContext를 활용한 앱 구조는 다음과 같습니다.


🌟 주요 컴포넌트 및 코드 구현

1️⃣ AppTodo.jsx

앱의 메인 컴포넌트로, TodoProvider를 통해 전역 상태를 제공합니다.

import "./App.css";
import TodoList from "./components/todo/TodoList";
import AddTodo from "./components/todo/AddTodo";
import { TodoProvider } from "./context/TodoContext";

function AppTodo() {
  return (
    <TodoProvider>
      <h2>할일 목록</h2>
      <AddTodo />
      <TodoList />
    </TodoProvider>
  );
}

export default AppTodo;

2️⃣ AddTodo.jsx

새로운 할 일을 추가하는 컴포넌트로, useTodosuseDispatch를 사용하여 상태를 업데이트합니다.

import { useState } from "react";
import { useDispatch, useTodos } from "../../context/TodoContext";

export default function AddTodo() {
  const [todoText, setTodoText] = useState("");
  const todos = useTodos();
  const dispatch = useDispatch();

  const handleAddTodo = (text) => {
    dispatch({
      type: "added",
      nextId: todos.length,
      todoText: text,
    });
    setTodoText("");
  };

  return (
    <div>
      <input
        type="text"
        value={todoText}
        onChange={(e) => setTodoText(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Enter" && !e.nativeEvent.isComposing) {
            handleAddTodo(todoText);
          }
        }}
      />
      <button onClick={() => handleAddTodo(todoText)}>추가</button>
    </div>
  );
}

3️⃣ TodoList.jsx

할 일 목록을 렌더링하는 컴포넌트입니다.
useTodos를 통해 상태를 가져오고, 각 항목은 TodoItem 컴포넌트로 구성됩니다.

import TodoItem from "./TodoItem";
import { useTodos } from "../../context/TodoContext";

function TodoList() {
  const todos = useTodos();
  return (
    <ul>
      {todos.map((item) => (
        <li key={item.id}>
          <TodoItem item={item} />
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

4️⃣ TodoItem.jsx

할 일 항목을 표시하며, 완료 상태 토글과 삭제 기능을 제공합니다.

import { useDispatch } from "../../context/TodoContext";

export default function TodoItem({ item }) {
  const dispatch = useDispatch();

  const handleToggleTodo = (id, done) => {
    dispatch({ type: "done", id, done });
  };

  const handleDeleteTodo = (id) => {
    dispatch({ type: "deleted", id });
  };

  return (
    <label>
      <input
        type="checkbox"
        checked={item.done}
        onChange={(e) => handleToggleTodo(item.id, e.target.checked)}
      />
      <span>{item.done ? <del>{item.text}</del> : item.text}</span>
      <button onClick={() => handleDeleteTodo(item.id)}>X</button>
    </label>
  );
}

🌟 Reducer 정의

Reducer는 상태 변경 로직을 중앙에서 관리하며, 모든 상태 업데이트가 이곳에서 이루어집니다.

reducer/todo-reducer.jsx

export default function todoReducer(draft, action) {
  switch (action.type) {
    case "added":
      draft.push({ id: action.nextId, text: action.todoText, done: false });
      break;

    case "deleted":
      return draft.filter((item) => item.id !== action.id);

    case "done":
      const target = draft.find((item) => item.id === action.id);
      if (target) target.done = action.done;
      break;

    default:
      throw new Error(`알 수 없는 액션 타입: ${action.type}`);
  }
}

🌟 Context API 활용

1️⃣ TodoContext.jsx

TodoContextTodoDispatchContext를 생성하고, 이를 통해 상태와 디스패치를 전역적으로 관리합니다.

import { createContext, useContext } from "react";
import { useImmerReducer } from "use-immer";
import todoReducer from "../reducer/todo-reducer";

export const TodoContext = createContext(null);
export const TodoDispatchContext = createContext(null);

export function TodoProvider({ children }) {
  const [todos, dispatch] = useImmerReducer(todoReducer, [
    { id: 0, text: "HTML&CSS 공부하기", done: false },
    { id: 1, text: "자바스크립트 공부하기", done: false },
  ]);

  return (
    <TodoContext.Provider value={todos}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoContext.Provider>
  );
}

export function useTodos() {
  return useContext(TodoContext);
}

export function useDispatch() {
  return useContext(TodoDispatchContext);
}

🌟 코드 최적화 팁

  • useTodosuseDispatch 커스텀 훅 사용
    각 컴포넌트에서 TodoContextTodoDispatchContext를 직접 접근하는 대신, 커스텀 훅을 통해 간결하게 관리합니다.
export function useTodos() {
  return useContext(TodoContext);
}

export function useDispatch() {
  return useContext(TodoDispatchContext);
}
  • TodoProvider로 상태와 디스패치 일괄 제공
    상태와 디스패치를 별도로 전달하지 않고, TodoProvider를 사용해 간결하게 구성할 수 있습니다.

🎯 핵심 정리

  1. Reducer와 Context로 상태 관리 통합

    • 상태 업데이트 로직은 Reducer에서 관리합니다.
    • Context를 통해 상태와 디스패치를 필요한 곳에서 사용할 수 있습니다.
  2. 커스텀 훅으로 가독성 향상

    • useTodos()useDispatch()를 사용해 상태와 디스패치를 간편하게 가져옵니다.
  3. UI와 로직의 분리

    • UI 컴포넌트(AddTodo, TodoList, TodoItem)는 상태 관리 로직과 분리되어 유지보수가 쉽습니다.
  4. Immer를 활용한 불변성 유지

    • useImmerReducer를 사용해 상태 변경을 더 안전하고 직관적으로 관리합니다.
profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글