한 입 크기로 잘라 먹는 리액트StudyReact

[React 🔯] 11. Context

이규현2026-03-15
[React 🔯] 11. Context

Context

Context란?

컴포넌트 간의 데이터를 전달하는 또 다른 방법이다. 기존의 Props가 가지고 있던 단점을 해결할 수 있다.

Props Drilling

Props는 부모 -> 자식으로만 데이터를 전달할 수 있었다.

부모 -> 자식으로만 데이터를 전달할 수 있기때문에 데이터를 2중으로 전달을 해야했다.

여러개의 계층구조가 생기게되면 위에 사진과 같이 코드 하나를 수정하면 전부 찾아가서 수정을 해야한다. 이를 props가 드릴처럼 땅을 파고 내려가는 것 같다라고 해서 Props Drilling이라고 한다.

Context를 사용하면 아래 이미지와 같이 다이렉트로 필요한 데이터를 전달할 수 있다.

Context 적용하기

App.jsx

import './App.css';
import { useRef, useReducer, useCallback, createContext, useMemo } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';

const mokData = [
  {
    id: 0,
    isDone: false,
    content: 'React 공부하기',
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: 'Node.js 공부하기',
    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 TodoStateContext = createContext();
export const TodoDispatchContext = createContext();

function App() {
  const [todos, dispatch] = useReducer(reducer, mokData);
  const idRef = useRef(3);

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

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: 'UPDATE',
      targetId: targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: 'DELETE',
      targetId: targetId,
    });
  }, []);

  const memoizedDispatch = useMemo(() => {
    return { onCreate, onDelete, onUpdate };
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider value={memoizedDispatch}>
          <Editor />
          <List />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;

TodoStateContextTodoDispatchContext 두 개의 Context를 분리해서 만들었다. 상태와 함수를 하나로 합치면 함수만 필요한 컴포넌트도 todos가 바뀔 때마다 같이 리렌더링되기 때문에 분리한 것이다. onCreate, onUpdate, onDelete 세 함수를 useMemo로 하나의 객체로 묶어서 TodoDispatchContext에 전달했다. 의존성 배열이 []이라 앱 최초 마운트 시 딱 한 번만 객체가 생성되고, 이후 todos가 바뀌어도 이 객체의 참조는 유지된다.

Editor.jsx

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

const Editor = () => {
  const { onCreate } = useContext(TodoDispatchContext);
  const [content, setContent] = useState('');
  const contentRef = useRef();

  const onChangeContent = (e) => {
    setContent(e.target.value);
  };
  const onKeydown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  const onSubmit = () => {
    if (content === '') {
      contentRef.current.focus();
      return;
    }
    onCreate(content);
    setContent('');
  };
  return (
    <div className="Editor">
      <input
        ref={contentRef}
        value={content}
        onKeyDown={onKeydown}
        onChange={onChangeContent}
        placeholder="새로운 Todo..."
      />
      <button onClick={onSubmit}>추가</button>
    </div>
  );
};
export default Editor;

useContext(TodoDispatchContext)onCreate만 가져온다. TodoStateContext를 구독하지 않기 때문에 todos가 변경되어도 이 컴포넌트는 리렌더링되지 않는다.

List.jsx

import './List.css';
import TodoItem from './TodoItem';
import { useState, useMemo, useContext } from 'react';
import { TodoStateContext } from '../App';

const List = () => {
  const todos = useContext(TodoStateContext);
  const [search, setSearch] = useState('');

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const getFilteredDate = () => {
    if (search === '') {
      return todos;
    }
    return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()));
  };
  const filteredTodos = getFilteredDate();

  const { totalCount, doneCount, notDoneCount } = useMemo(() => {
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todos]);

  return (
    <div className="List">
      <h4>Todo List 📋</h4>
      <div>total: {totalCount}</div>
      <div>done:{doneCount}</div>
      <div>notDone: {notDoneCount}</div>
      <input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => {
          return <TodoItem key={todo.id} {...todo} />;
        })}
      </div>
    </div>
  );
};
export default List;

통계 계산(totalCount, doneCount, notDoneCount)을 useMemo로 감쌌다. todos가 바뀔 때만 다시 계산하고, 검색어가 바뀌어서 리렌더링될 때는 이전 계산값을 재사용한다.

TodoItem.jsx

import './TodoItem.css';
import { memo, useContext } from 'react';
import { TodoDispatchContext } from '../App';

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

  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  const onClickDeleteButton = () => {
    onDelete(id);
  };
  return (
    <div className="TodoItem">
      <input onChange={onChangeCheckbox} readOnly checked={isDone} type="checkbox" />
      <div className="content">{content}</div>
      <div classame="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

export default memo(TodoItem);

memo로 감싸서 부모인 List가 리렌더링되어도 props가 바뀐 항목만 리렌더링되도록 했다. useContext(TodoDispatchContext)onUpdate, onDelete를 가져오는데, 이 Context의 참조가 변하지 않기 때문에 memo의 최적화가 깨지지 않는다.