상세 컨텐츠

본문 제목

[리액터 스타터3] 9장 컴포넌트 트리에 데이터 공급하기

23-24/React.js 3

by 롱롱😋 2024. 1. 5. 10:00

본문

728x90

9장 컴포넌트 트리에 데이터 공급하기

 

 

Context

  • 상태 변화 코드를 쉽게 분리할 수 있다.

 

Context를 사용하는 이유

  • 'Props Drilling' 문제를 해결하기 위해
  • Props Drilling : Props는 부모 컴포넌트에서 자식 컴포넌트로 전달되어야 하는데, 때로는 중간에 있는 여러 컴포넌트를 통해 전달되어야 하는 경우가 있다. 이때 발생하는 것이 Props Drilling 문제이다. 

 

Context란?

  • 같은 문맥 아래에 있는 컴포넌트 그룹에 데이터를 공급하는 기능 
  • ex) project2의 Body, Sidebar, Main은 모두 "할 일을 관리한다"라는 문맥 아래에서 동작하는 컴포넌트 

 

ContextAPI 

  • Context를 만들고 다루는 리액트 기능
  • Context 만들기
import React from 'react'; // 라이브러리의 React객체 불러오기 
const MyContext = React.createContext(defaultValue); // createContext 메서드를 호출해 새로운 Context 만들기

 

  • Context에 데이터 공급하기 

 provider 컴포넌트는 Props로 공급할 데이터를 받아, 컴포넌트 트리에서 자신보다 하위에 있는 모든 컴포넌트에 데이터를 공급한다. 

import React from "react";

const MyContext = React.createContext(defaultValue);

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      <MyContext.provider value={data}>
        <Body />
      </MyContext.provider>
    </div>
  );
}
export default App;

 

  • Context가 공급하는 데이터 사용하기 
import React, { useContext } from "react"; // useContext를 react 라이브러리에서 불러오기

const MyContext = React.createContext(defaultValue);

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      <MyContext.provider value={data}>
        <Body />
      </MyContext.provider>
    </div>
  );
}

function Main() {
    const data = useContext(MyContext); // useContext를 호출하고 Context를 전달, useContext는 해당 Context가 공급하는 값을 반환
    (...)
}

export default App;

 

 

Context를 이용한 데이터 공급 구조

  1. createContext를 이용해 Context를 만들기 
  2. 값을 공급할 컴포넌트를 Context.Provider로 감싸기 
  3. 함수 useContext를 호출해 Context가 공급하는 값을 불러와 사용하기 

 

 

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

  • 리팩토링: 사용자에게 제공하는 기능은 변경하지 않으면서 내부 구조를 개선하는 작업 

 

어떻게 Context를 적용할지 생각해 보기

  • 기존의 방식은 TodoList를 통해 TodoITem에게 onDelete와 onUpdate를 전달해야 하는 props Drilling 문제가 발생 
  • TodoContext라는 이름의 Context를 생성하고, App 컴포넌트 하위에 있는 TodoContext.Provider 밑에 TodoEditor, TodoList, TodoItem 컴포넌트를 배치 

 

TodoContext를 만들어 데이터 공급하기

  • App.js에서 TodoContext를 생성 
  • 반드시 컴포넌트 밖에서 생성해야 한다. 
// App.js

import React, { useReducer, useRef, useCallback } from "react";
(...) 
const TodoContext = React.createContext(); // TodoContext 만들기 

function App() {
	(...) 
} 

export default App;

 

 

  • App 컴포넌트에서 Provider를 배치해 데이터 공급 설정 
  • Context에서 데이터를 받으므로 기존의 Props는 모두 제거 
// App.js

import React, { useReducer, useRef, useCallback } from "react";
(...) 
const TodoContext = React.createContext(); 

function App() {
	(...) 
      return (
    <div className="App">
      <Header />
      <TodoContext.Provider value={{ todo, onCreate, onUpdate, onDelete }}> // Props(value)를 객체로 설정
        <TodoEditor /> // 기존의 Props 제거 
        <TodoList /> // 기존의 Props 제거 
      </TodoContext.Provider>
    </div>
  );
} 

export default App;

 

 

  • "Cannot read properties of undefined (reading 'length')"라는 오류가 발생할 것 
  • TodoList 컴포넌트에서 객체가 아닌 undefined값에 length 프로퍼티로 접근하기 때문에 발생한 오류 
  • 다음과 같이 Todo의 기본값을 빈 배열로 하는 defaultProps를 설정하여 오류를 해결 
// TodoList.js

const TodoList = ({ todo, onUpdate, onDelete }) => { 
(...)
}

TodoList.defaultProps = {
    todo: [], 
}; 

export default TodoList;

 

 

 

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

  • 다른 파일에서 불러올 수 있도록 TodoContext를 export로 내보내기 
// App.js

(...)
export const TodoContext = React.createContext();
(...)

 

 

  • TodoList 컴포넌트에서 TodoContext를 불러오고, useContext를 이용해 Context가 공급하는 데이터 가져오기 
  • TodoList에서도 모든 데이터를 불러오는 것을 확인하면 console.log(storeData)는 삭제 
// TodoList.js

import { useContext, useState, useMemo } from "react"; // useContext 불러오기 
import { TodoContext } from "../App"; // TodoContext 불러오기 
(...) 
const TodoList = ({ todo, onUpdate, onDelete }) => {
    const storeData = useContext(TodoContext); // useContext를 호출하고 TodoContext를 인수로 전달해 storeData에 저장하기  
    console.log(storeData); // storeData를 콘솔에 출력
    (...) 
}; 
(...)

 

 

  • useContext로 데이터를 사용할 수 있으므로, 다음과 같이 수정 
// TodoList.js

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

export default TodoList;

 

 

 

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

  • TodoList에서 onDelete와 onUpdate을 전달할 필요성이 없으므로, TodoList를 다음과 같이 수정 
// TodoList.js

const TodoList = () => {
    const { todo } = useContext(TodoContext); // todo 외의 나머지 삭제 
    (...) 
     return (
    <div className="TodoList">
        (...) 
        <div className="list_wrapper">
            {
                getSearchResult().map((it)=> (
                    <TodoItem key={it.id} {...it} /> // 기존에 Props으로 전달하던 코드도 삭제
                ))
            }
        </div>
    </div>
    ); 
}; 

export default TodoList;

 

 

  • TodoItem 컴포넌트에서 useContext로 함수  onDelete와 onUpdate을 받아 사용할 수 있도록 수정 
// TodoItem.js

import React, { useContext } from "react"; // useContext 불러오기 
import { TodoContext } from "../App"; // TodoContext 불러오기 
(...) 
const TodoItem = ({ id, content, isDone, createDate }) => { // onDelete, onUpdate 삭제 
    const { onDelete, onUpdate } = useContext(TodoContext); // useContext을 호출해 TodoContext의 값을 불러와 구조 분해 할당
    (...) 
}; 

export default React.memo(TodoItem);

 

 

 

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

  • TodoEditor 컴포넌트에서 할 일 아이템을 생성하는 함수 onCreate를 TodoContext에서 받도록 수정 
// TodoEditor.js

import { useContext, useState, useRef } from "react"; // useContext 불러오기 
import { TodoContext } from "../App"; // TodoContext 불러오기 
(...)
const TodoEditor = () => {
    const { onCreate } = useContext(TodoContext); // useContext을 호출해 TodoContext의 값을 불러와 구조 분해 할당
    (...) 
}; 
(...)

 

 

 

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

  • 이전과 같이 기능이 정상적으로 동작하는지 확인 
  • 최적화에도 문제가 없는지 점검 
  • 최적화를 위해 TodoItem 컴포넌트에 적용했던 React.memo가 제대로 동작하는지 확인 
// TodoItem.js

(...)
const TodoItem = ({ id, content, isDone, createDate }) => {
    console.log(`${id} TodoItem 업데이트`); // 컴포넌트를 렌더링할 때마다 콘솔 메세지 출력
    (...)
}; 

export default React.memo(TodoItem);

 

  • 할 일 아이템을 생성한 후 콘솔을 보면, 모든 TodoItem 컴포넌트가 리렌더 되는 것을 확인  
  • 최적화에 문제 발생 

 

 

 

 

 

문제의 원인 파악하기

  • 문제점: 리팩토링 이후 TodoItem 컴포넌트가 불필요한 상황에서도 리렌더 됨 
  • Context의 Provider 또한 리액트 컴포넌트이므로 Props로 전달되는 Value 값이 바뀌면 리렌더 되고, TodoContext.Provider 아래의 컴포넌트들도 함께 리렌더 됨 

 

 

 

구조 재설계하기

  • 문제의 원인: State 변수 todo와 onCreate, onUpdate, onDelete와 같은 dispatch 관련 함수들이 하나의 객체로 묶여 동일한 Context에 Props로 전달되지 때문 
  • Context를 역할에 따라 분리하는 게 바람직 
  • TodoStateContext: todo가 업데이트되면 영향받는 컴포넌트를 위한 Context
  • TodoDispatchContext: dispatch 함수 onCreate, onUpdate, onDelete가 변경되면 영향을 받는 컴포넌트를 위한 Context

 

 

재설계된 구조로 변경하기

  • TodoContext는 삭제
  • todo를 공급할 TodoStateContext와 dispatch를 공급할 TodoDispatchContext를 각각 만들어 배치 
  • 저장하면 오류가 발생하나 곧 해결할 것이므로 무시 
// App.js

export const TodoStateContext = React.createContext(); // TodoStateContext 만둘기
export const TodoDispatchContext = React.createContext(); // TodoDispatchContext 만들기

function App() {
(...)
  return (
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={{ todo }}> // Props 전달
        <TodoDispatchContext.Provider value={{ onCreate, onUpdate, onDelete }}> // Props 전달
          <TodoEditor />
          <TodoList />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;

 

 

  • Todo가 변경되어 App 컴포넌트를 리렌더 하면 TodoDispatchContext.Provider에 Props로 전달하는 3개의 함수를 다시 생성함
  • 따라서 useMemo를 이용해 TodoDispatchContext.Provider에 전달할 dispatch 함수를 다시 생성하지 않도록 해야 함\
  • 이번에도 오류가 발생  
// App.js

import React, { useMemo, useReducer, useRef, useCallback } from "react"; // useMemo 불러오기 
(...) 
function App() {
	(...) 
      const memoizedDispatches = useMemo(()=> { // useMemo로 onCreate, onUpdate, onDelete를 묶기 
    	return { onCreate, onUpdate, onDelete }; 
  }, []); 

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

export default App;

 

 

  • TodoEditor 컴포넌트는 App에서 할 일 아이템을 생성하는 함수 onCreate가 필요 
  • 따라서 TodoDispatchContext에서 함수 onCreate만 받으면 됨 
// TodoEditor.js

(...)
import { TodoDispatchContext } from "../App"; // TodoDispatchContext 불러오기 
(...) 
const TodoEditor = () => {
    const { onCreate } = useContext(TodoDispatchContext); // useContext를 호출하여 TodoDispatchContext에서 함수 onCreate 불러오기
    (...) 
}; 

export default TodoEditor;

 

 

 

  • TodoList 컴포넌트는 할 일 데이터인 todo를 사용하므로, 이것을 TodoStateContext에서 받도록 수정 
// TodoList.js

(...) 
import { TodoStateContext } from "../App"; // TodoStateContext 불러오기 
(...) 

const TodoList = () => {
    const todo = useContext(TodoStateContext); // TodoStateContext 데이터 가져오기, todo 배열 그 자체를 전달 
    (...) 
}; 

export default TodoList;

 

 

  • TodoItem 컴포넌트는 할 일을 수정하고 삭제하는 함수 onUpdate와 onDelete를 사용하므로 TodoDispatchContext에서 해당 함수를 받도록 수정 
// TodoItem.js

(...)
import { TodoDispatchContext } from "../App"; // TodoDispatchContext 가져오기 
(...)

const TodoItem = ({ id, content, isDone, createDate }) => {
    console.log(`${id} TodoItem 업데이트`); 
    const { onDelete, onUpdate } = useContext(TodoDispatchContext); // TodoDispatchContext의 데이터 중 onDelete, onUpdate를 불러옴
    (...) 
}; 

export default React.memo(TodoItem);

 

 

 

리팩토링이 잘 되었는지 확인하기 2

  • 새로운 할 일 아이템을 생성하면 생성한 TodoItem만 렌더링 하고 나머지 컴포넌트는 더 이상 리렌더 되지 않으면 성공 

 

 


Quiz 

1. ( Context )란 같은 문맥 아래에 있는 컴포넌트 그룹에 데이터를 공급하는 기능이다. 

2. [1번 답]을 사용하는 이유는 ( Props Drilling 문제 )를 해결하기 위해서다. 

3. [2번 답]이란 Props가 ( 중간에 있는 여러 컴포넌트 )를 통해 전달되어야 하는 경우이다.

4. ( ContextAPI )란 Context를 만들고 다루는 리액트 기능이다. 

5.  ( provider 컴포넌트 )는 Props로 공급할 데이터를 받아, 컴포넌트 트리에서 자신보다 하위에 있는 모든 컴포넌트에 데이터를 공급한다. 

6. Context를 이용한 데이터 공급 구조는 ( createContext )를 이용해 Context를 만들기 -> 값을 공급할 컴포넌트를 ( Context.Provider)로 감싸기 -> 함수 ( useContext )를 호출해 Context가 공급하는 값을 불러와 사용하기이다. 

 

 

프로그래밍 문제

1.  Context를 생성하고, provider 컴포넌트를 이용하여 Props로 공급할 데이터를 받아 컴포넌트 트리에서 자신보다 하위에 있는 Body 컴포넌트에 데이터를 공급하시오. 

import React from "react";

// 여기에 코드 작성 

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      // 여기에 코드 작성 
    </div>
  );
}
export default App;

 

 

2. MyContext가 공급하는 데이터를 사용하시오. 

import React, { useContext } from "react"; 

const MyContext = React.createContext(defaultValue);

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      <MyContext.provider value={data}>
        <Body />
      </MyContext.provider>
    </div>
  );
}

function Main() {
   // 여기서 코드 작성 
    (...)
}

export default App;

 

 


1.

import React from "react";

const MyContext = React.createContext(defaultValue);

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      <MyContext.provider value={data}>
        <Body />
      </MyContext.provider>
    </div>
  );
}
export default App;

 

 

 

2. 

import React, { useContext } from "react"; 

const MyContext = React.createContext(defaultValue);

function App() {
  const data = "data";
  return (
    <div>
      <Header />
      <MyContext.provider value={data}>
        <Body />
      </MyContext.provider>
    </div>
  );
}

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

export default App;

 

 

 

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

Corner React.js 3 

Editor: lyonglyong

728x90

관련글 더보기