DEVELOPMENT/react

13. 컴포넌트 트리에 데이터 공급하기

Tiny Commit 2025. 1. 8. 13:36

 

 

 

1. Context 

Context: 컨포넌트 트리 전체를 대상을 데이터를 공급하는 기능

 

1. Context를 사용하는 이유

Props Drilling문제를 해결하기 위해

  • 리액트 컴포넌트 계층 구조에서 컴포넌트 간에 값을 전달할 때 발생합니다.
  • 컨포넌트 간에 데이터를 전달할때 Props를 사용합니다. (부모 -> 자식: 단방향)
  • 컨포넌트 사이의 데이터 교환구조를 파악하기 어렵게 만듭니다. 
  • Props를 수정하면 여러 컨포넌트를 살펴봐야 하므로 코드 유지 보수를 어렵게 합니다. 

 

2. Context란?

Context: 같은 문맥 아래에 있는 컴포넌트 그룹에 데이터를 공급하는 기능

  • A문장과 B문장이 동일한 문맥 아래에 있다  =  A와 B가 동일한 목적(기능)을 가지고 있다.
  • Props Drilling문제 해결가능

 

 

 

3. ContextAPI

ContextAPI: Context를 만들고 다루는 리액트 기능입니다. 

 

<Context 만들기>

import React from 'react';
const MyContext = React.createContext(defaultValue);

 

<Context에 데이터 공급하기>

  • Context.Provider기능 사용합니다.
  • Provider 컴포넌트는 Props로 공급할 데이터를 받아, 컴포넌트 트리에서 자신보다 하위에 있는 모든 컴포넌트에 데이터를 공급한다.
import React from 'react';
const MyContext = React.createContext(defaultValue);

function App() {
  const data = 'data';
  return (
    <div>
      <Header/>
      <MyContext.Provider vakue={data}>
        <Body/>
      </MyContext.Provider>
    </div>
  );
}
export default App;

 

 

<Context가 공급하는 데이터 사용하기>

  • useContext를 사용하여 자신이 속한 그룹의 Context가 공급하는 데이터를 불러옵니다. 
import React, {useContext} from 'react';
const MyContext = React.createContext(defaultValue);

function App() {
  const data = 'data';
  return (
    <div>
      <Header/>
      <MyContext.Provider vakue={data}>
        <Body/>
      </MyContext.Provider>
    </div>
  );
}

fouction Main() {
  const data = useContext(MyContext);
  (...)
}

export default App;

 

 

 

 

 

 

 

 

2. Context 로 [할 일 관리]앱 리팩토링하기

리팩토링이란? 사용자에게 제공하는 기능은 변경하지 않으면서 내부 구조를 개선하는 작업입니다.

 

[할 일 관리]앱

  • 데이터 전달 구조: State와 Props로만 이루어져 있어 Props Drilling문제 방생
  • 개선: Props Drilling을 제거하면서 추가, 수정, 삭제, 검색 기능은 변함 x

1. 어떻게 Context를 적용할지 생각하기

 

 

2. TodoCcontext를 만들어 데이터를 공급하기

<TodoContext 만들기>
<데이터 공급하기>

// src/App.js
// 1.2 TodoCcontext를 만들어 데이터를 공급하기

import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import React, { useCallback, useReducer, useRef } from "react";


const mockTodo = [ 
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래 널기",
    createdDate: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    createdDate: new Date().getTime(),
  },
];

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

const TodoContext = React.createContext();

function App() {

  const [todo, dispatch] = useReducer(reducer, mockTodo);
  const idRef = useRef(3);

  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      newItem: {
        id: idRef.current,
        content,
        isDone: false,
        createDate: new Date().getTime(),
      },
    })
    idRef.current += 1; 
  };
  const onUpdate = useCallback((targetId) => { 
    dispatch({
      type: "UPDATE",
      targetId,
    });
},[]);
  const onDelete = useCallback((targetId) => { 
    dispatch({
      type: "DELETE",
      targetId,
    });
  },[]);

  return (
    <div className="App">
      <Header />
      <TodoContext.Provider value={{ todo, onCreate, onUpdate, onDelete }}>
        <TodoEditor />
        <TodoList /> 
      </TodoContext.Provider>
    </div>
  );
}
export default App;
// 2025-01-08
// src/component/TodoList.js
// 1.2 TodoCcontext를 만들어 데이터를 공급하기
import { useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo = [], onUpdate, onDelete }) => {
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { 
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase())
        );
  };
  const analyzeTodo = useMemo(() => {
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todo]);
  const {totalCount, doneCount, notDoneCount} = analyzeTodo;


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem
            key={it.id}
            {...it}
            onUpdate={onUpdate}
            onDelete={onDelete} 
          />
        ))}
      </div>
    </div>
  );
};

TodoList.defaultProps = {
  todo: []
};

export default TodoList;

 

3. TodoList 컴포넌트에서 Context 데이터 사용하기

(데이터를 꺼내 사용하기)

// src/App.js
export const TodoContext = React.createContext();

 

// src/component/TodoList.js
import { useContext, useMemo, useState } from "react";
import {TodoContext} from "../App.js";
(...)

const TodoList = () => {
  const { todo, onUpdate, onDelete } = useContext(TodoContext);
  const storeData = useContext(TodoContext);
(...)

 

 

4. TodoItem 컴포넌트에서 Context 데이터 사용하기

  • 이전에는 onUpdate와 onDelete를 TodoList에서 받아서 사용한 반면, 이제는 Context에서 직접 불러와 사용할 수 있습니다.
  • 함수들을 전달할 필요가 없으므로 Props로 받을 필요가 없습니다.
// src/component/TodoList.js
// 1.4 TodoItem 컴포넌트에서 Context 데이터 사용하기
import { useContext, useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
import {TodoContext} from "../App.js";

const TodoList = () => {
  const { todo } = useContext(TodoContext);
  const storeData = useContext(TodoContext);
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { 
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase())
        );
  };
  const analyzeTodo = useMemo(() => {
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todo]);
  const {totalCount, doneCount, notDoneCount} = analyzeTodo;


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem key={it.id} {...it} />
        ))}
      </div>
    </div>
  );
};

TodoList.defaultProps = {
  todo: []
};

export default TodoList;
// src/component/TodoItem.js
// 1.4 TodoItem 컴포넌트에서 Context 데이터 사용하기
import "./TodoItem.css";
import React, { useContext} from "react";
import {TodoContext} from "../App.js";

const TodoItem = ({ id, content, isDone, createdDate}) => { 
  const {onUpdate, onDelete} = useContext(TodoContext);
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
  const onClickDelete = () => { 
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()}
      </div>
      <div className="btn_col">
        <button onClick={onClickDelete}>삭제</button> 
      </div>
    </div>
  );
};
export default React.memo(TodoItem);

 

 

5. TodoEditor 컴포넌트에 데이터 공급하기

// 2025-01-08
// src/component/TodoItem.js
// 1.5 TodoEditor 컴포넌트에 데이터 공급하기
import { useContext, useState, useRef } from "react";
import "./TodoEditor.css";
import {TodoContext} from "../App.js";

const TodoEditor = () => {
  const { onCreate } = useContext(TodoContext);
  const [content, setContent] = useState(""); 
  const inputRef = useRef();
  const onChangeContent = (e) => { 
    setContent(e.target.value);
  };
  
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent(""); 
  };
  const onKeyDown = (e) => { 
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  return(
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input
          ref={inputRef}
          value={content}
          onChange={onChangeContent}
          onKeyDown={onKeyDown} 
          placeholder="새로운 Todo..."
        />
        <button onClick={onSubmit}>추가</button>
      </div>
    </div>
  )
};
export default TodoEditor;

 

 

 

6. 리팩토링 잘 되었는지 확인하기1

지금까지 모두 오류없이 실행이 되었다면 잘 리팩토링이 되었다.

이제 최적화를 해보겠다.

 

최적화를 위해 React.memo가 리팩토링 이후에도 제대로 동작하는지 다시 확인해야 합니다.

-> 모든 TodoItem컴포넌트가 리렌더된다.

-> React.memo가 리팩토링 이후 정상적으로 동작하지 않았다. 

 

7.  문제의 원인 찾기

Context 리팩토링 이후 TodoItem 컴포넌트가 불필요한 상황에서도 리렌더 되고 있다는 점입니다.

Props의 Value값이 변경 -> Context.Provider 리렌더

 

 

8.  구조 재설계하기

원인: State변수 todo와 onCreate, onUpdate, onDelete와 같은 dispatch관련 함수들이 하나의 객체로 묶여 동일한 Context에 Props로 전달되는 것입니다. 

 

<해결>

  • TodoStateContext: todo가 업데이트 되면 영향받는 컴포넌트를 위한  Context
  • TodoDispatchContext: dispatch함수 onCreate, onUpdate, onDelete가 변경되면 영향을 받는 컴포넌트를 위한 Context

 

 

 

9.  재설계된 구조로 변경하기

<Context분리하기>

// src/App.js
// 1.9  재설계된 구조로 변경하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import React, { useMemo, useCallback, useReducer, useRef } from "react";

const mockTodo = [ 
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래 널기",
    createdDate: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    createdDate: new Date().getTime(),
  },
];

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

//export const TodoContext = React.createContext();
export const TodoStateContext = React.createContext();
export const TodoDispatchContext = React.createContext();

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

  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      newItem: {
        id: idRef.current,
        content,
        isDone: false,
        createDate: new Date().getTime(),
      },
    })
    idRef.current += 1; 
  };
  const onUpdate = useCallback((targetId) => { 
    dispatch({
      type: "UPDATE",
      targetId,
    });
},[]);
  const onDelete = useCallback((targetId) => { 
    dispatch({
      type: "DELETE",
      targetId,
    });
  },[]);
  const memoizedDispatches = useMemo(() => {
    return { onCreate, onUpdate, onDelete };
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={todo}>
        <TodoDispatchContext.Provider value={memoizedDispatches}>
          <TodoEditor />
          <TodoList /> 
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}
export default App;

 

  • todo가 변경되어 App컨포넌트를 리렌더라면 TodoDispatchContex 에 Peops를 전달하는 3개의 함수를 다시 생성합니다. 
  • useMemo를 이용해 TodoDispatchContext.Provider에 전달할 dispatch함수를 다시 생성하지 않도록 만들어야 합니다.
  • useCallback을 적용한 함수 onUpdate, onDelete는 다시 생성되지 않으나, Props로 전달하지 위해 묶은 3개의 함수 객체는 다시 생성됩니다. (따라서 useMemo 사용)

 

<TodoEditor 수정하기>

  • TodoStateContext에서 데이터를 받을 필요는 없으며, TodoDispatchContext에서 함수 onCreate만 받으면 됩니다.
// src/component/TodoItem.js
// 1.9  재설계된 구조로 변경하기
import { TodoDispatchContext } from "../App";
import { useContext, useState, useRef } from "react";
import "./TodoEditor.css";


const TodoEditor = () => {
  const { onCreate } = useContext(TodoDispatchContext);
  const [content, setContent] = useState(""); 
  const inputRef = useRef();
  
  const onChangeContent = (e) => { 
    setContent(e.target.value);
  };
  
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent(""); 
  };
  const onKeyDown = (e) => { 
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  return(
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input
          ref={inputRef}
          value={content}
          onChange={onChangeContent}
          onKeyDown={onKeyDown} 
          placeholder="새로운 Todo..."
        />
        <button onClick={onSubmit}>추가</button>
      </div>
    </div>
  )
};
export default TodoEditor;

 

 

<TodoList 수정하기>

  • todo를 TodoStateContext에서 받도록 수정
// src/component/TodoList.js
// 1.9  재설계된 구조로 변경하기
import { useContext, useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
import {TodoStateContext} from "../App";

const TodoList = () => {
  const  todo  = useContext(TodoStateContext);
  const storeData = useContext(TodoStateContext);
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { 
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase())
        );
  };
  const analyzeTodo = useMemo(() => {
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todo]);
  const {totalCount, doneCount, notDoneCount} = analyzeTodo;


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem key={it.id} {...it} />
        ))}
      </div>
    </div>
  );
};

TodoList.defaultProps = {
  todo: []
};

export default TodoList;

 

 

<TodoItem 수정하기>

  • onUpdate와 onDelete를 TodoDispatchContext에서 해당 함수를 받도록 수정
// src/component/TodoItem.js
// 1.9 재설계된 구조로 변경하기
import "./TodoItem.css";
import React, { useContext} from "react";
import {TodoDispatchContext} from "../App";

const TodoItem = ({ id, content, isDone, createdDate}) => { 
  console.log(`${id} TodoItem 업데이트`);
  const {onUpdate, onDelete} = useContext(TodoDispatchContext);
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
  const onClickDelete = () => { 
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()}
      </div>
      <div className="btn_col">
        <button onClick={onClickDelete}>삭제</button> 
      </div>
    </div>
  );
};
export default React.memo(TodoItem);

 

 

 

 

 

 


출처 :  이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).