본문으로 바로가기

[REACT] Context 로 전역 상태 관리하기

category 나의 주니어 개발 일기/REACT 2025. 7. 16. 11:08
728x90
반응형
SMALL

Context 란


 

Props의 데이터 전달 방식은 서비스의 부모가 커지고 컴포넌트 간의 관계가 복잡해질 경우 데이터를 전달하기 매우 비효율적이게 된다.

이 비효율을 Context 를 통해서 해결할 수 있다.


이런식으로 Context도 분리해서 관리할 수 있다.


Props 방식의 App.jsx

import './App.css'
import Header from "./components/Header"
import Editor from "./components/Editor"
import List from "./components/List"

import { 
  useState,
  useRef,
  useReducer,
  createContext
 } from 'react'
const mockData = [
    {
      id: 0,
      isDone: false,
      content: "React 공부하기",
      date: new Date().getTime(),
    },
    {
      id: 1,
      isDone: false,
      content: "청소하기",
      date: new Date().getTime(),
    },
    {
      id: 2,
      isDone: false,
      content: "빨래하기",
      date: new Date().getTime(),
    },
  ];

function reducer(state, action){
  switch(action.type){
    case 'CREATE': return [action.data, ...state];
    case 'UPDATE': return state.map((item) => item.id === action.targetId? {...item, isDone: !item.isDone}:item);
    case 'DELETE': return state.filter((item)=> item.id !== action.targetId);
    default: return state;
  }
}

export const TodoContext = createContext(); //다른 컴포넌트에서 자유롭게 Context 사용 가능
console.log(TodoContext);

function App() {
  //상태를 관리할 때, 배열안에 객체가 들어가는 복잡한 구조 같은것을 사용한다면 일반적으로 useReducer를
  //카운팅 처럼 단순 상태를 관리한다면 useState 를 사용한다.
  const [todos, dispatch] = useReducer(reducer, mockData);
  const idRef = useRef("3");

  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      },
    });
  }

  const onUpdate = (targetId) => {
    console.log(targetId);
    dispatch({
      type: "UPDATE",
      targetId: targetId,
    });
  }

  const onDelete = (targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }

  return (
    <div className='App'>
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App

Context 방식의 App.jsx

import { 
  ...
 ,createContext
 } from 'react'

const TodoContext = createContext();

function App() {
    ...

  return (
    <div className='App'>
      <Header />

      <TodoContext.Provider
       value={{
          todos,
          onCreate,
          onUpdate,
          onDelete
        }}
      >
        <Editor onCreate={onCreate} />
        <List
          todos={todos}
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      </TodoContext.Provider>
    </div>
  );
}

export default App

App 컴포넌트가 리랜더링 될 때마다 context가 변하면 안되기 때문에 App 컴포넌트 밖에 선언한다.


기존에 넘겨줘야 했던 데이터들(onCreate, todos 등)이 Context.provider 내에 선언된 것을 확인 할 수 있다.


그러면 기존에 props를 이용해서 데이터를 전달받던 컴포넌트들도 수정해줘야한다.

Editor 컴포넌트를 참고하자

import { 
  ...
 ,createContext
 } from 'react'

export const TodoContext = createContext(); //다른 컴포넌트에서 자유롭게 Context 사용 가능

function App() {
    ...
    <div className='App'>
      <Header />

      <TodoContext.Provider
        value={{
            todos,
            onCreate,
            onUpdate,
            onDelete
          }}
      >
        <Editor />
        <List />
      </TodoContext.Provider>
    </div>
  );
}

export default App

이런식으로 선언을 해두고

export const TodoContext = createContext(); //다른 컴포넌트에서 자유롭게 Context 사용 가능

다른 컴포넌트에서는 호출해서 사용할 수 있다.

import { TodoContext } from '../App';

Props 방식의 Editor.jsx

import './Editor.css'
import { useState, useRef } from 'react';

const Editor = ({onCreate}) => {
    ...
};

export default Editor;

Context 방식의 Editor.jsx

import './Editor.css'
import { useState, useRef, useContext } from 'react';
import { TodoContext } from '../App';

const Editor = () => {
    const { onCreate } = useContext(TodoContext);
    ...
}

Context 를 적용했더니 이전의 memo 최적화 부분이 풀렸다 왜 일까?? Context 분리하기 장을 참고하자


Context 분리하기

TodoContext,Provider 도 결국 App 컴포넌트의 자식이다.

때문에 todos, onCreate 같은 App 컴포넌트의 Props가 바뀌면 결국 TodoContext.Provider 컴포넌트도 리랜더링 되고 그 자식들인 Editor, List, TodoItem 모두 리랜더링 된다.


TodoItem 에는 memo를 적용했는데도 불구하고 왜 리랜더링이 발생하지?

export default memo(TodoItem);

App이 리랜더링 될경우 TodoContext.Provider 객체 자체가 다시 생성된다.

      <TodoContext.Provider
        value={{
            todos,
            onCreate,
            onUpdate,
            onDelete
          }}
      >
      ...
      </TodoContext.Provider>

때문에 그대로 TodoItem에서의 context 객체도 다시 생성된다, 결국 memo를 적용해도 최적화가 풀린다,

const TodoItem = ({id, isDone, content, date}) => {
  const { onUpdate, onDelete } = useContext(TodoContext);
   ...
}

이럴 경우 context를 한번 더 분리해야 한다.


위 그림 대로 App 컴포넌트에서 context를 2개로 분리하자

App.jsx

import './App.css'
import Header from "./components/Header"
import Editor from "./components/Editor"
import List from "./components/List"

import { 
  useMemo,
  useState,
  useRef,
  useReducer,
  createContext
 } from 'react'

//2개로 분리
export const TodoStateContext = createContext();  //변할 수 있는 값
export const TodoDispatchContext = createContext(); //변할 수 없는 값

console.log(TodoContext);

const mockData = [
    {
      id: 0,
      isDone: false,
      content: "React 공부하기",
      date: new Date().getTime(),
    },
    {
      id: 1,
      isDone: false,
      content: "청소하기",
      date: new Date().getTime(),
    },
    {
      id: 2,
      isDone: false,
      content: "빨래하기",
      date: new Date().getTime(),
    },
  ];

function reducer(state, action){
  switch(action.type){
    case 'CREATE': return [action.data, ...state];
    case 'UPDATE': return state.map((item) => item.id === action.targetId? {...item, isDone: !item.isDone}:item);
    case 'DELETE': return state.filter((item)=> item.id !== action.targetId);
    default: return state;
  }
}


function App() {
  //상태를 관리할 때, 배열안에 객체가 들어가는 복잡한 구조 같은것을 사용한다면 일반적으로 useReducer를
  //카운팅 처럼 단순 상태를 관리한다면 useState 를 사용한다.
  const [todos, dispatch] = useReducer(reducer, mockData);
  const idRef = useRef("3");

  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      },
    });
  }

  const onUpdate = (targetId) => {
    console.log(targetId);
    dispatch({
      type: "UPDATE",
      targetId: targetId,
    });
  }

  const onDelete = (targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }

  const memoizedDispatch = useMemo(() => {
    return { onCreate, onUpdate, onDelete};
  }, []); //2번째 인자는 빈 배열을 적용해서, App 컴포넌트에 마운트 된 이후에는 재생성되지 않도록 한다.

  return (
    <div className='App'>
      <Header />

      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider
          value={memoizedDispatch}
        >
          <Editor />
          <List />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App

Editor.jsx

import { TodoDispatchContext } from '../App.jsx';

const Editor = () => {
    const { onCreate } = useContext(TodoDispatchContext);
    ...
}

List.jsx

import { TodoStateContext } from '../App';


const List = () => {
    const todos  = useContext(TodoStateContext); //       <TodoStateContext.Provider value={todos}> 이렇게 단일 값으로 전달되고, 객체가 아니기 때문에 구조 분해 할당 const { todos } 으로 선언 안해도 된다.
    ...
}    

TodoItem.jsx

import { TodoDispatchContext } from '../App';


const TodoItem = ({id, isDone, content, date}) => {
  const { onUpdate, onDelete } = useContext(TodoDispatchContext);
  ...
}

 

 

 

 

 

그림출처: https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/dashboard

728x90
반응형
LIST