본문 바로가기

코딩 공부/React.js 리액트

[6주차] useReducer, 최적화, context

useReducer:

컴포넌트 내부에 새로운 State를 생성하는 React Hook, 모든 useState는 useReducer로 대체 가능

상태 관리 코드를 컴포넌트 외부로 분리할 수 있음

- 리액트 컴포넌트의 주요 역할은 UI를 렌더링하는 것, 

따라서 스테이트(onCreate, onUpdate 등)를 관리하는 코드가 너무 많아지면 주객 전도된 상황이 된다

 

dispatch 호출, reducer 호출, 액션 객체 reducer로 전달

- dispatch로 상태 변화 요청, 요청 내용은 안에 있는 객체(1씩 증가) <- 액션객체

- reduce함수에서 새로운 state의 값을 반환하면 state의 값이 변경된다

function reducer(state, action) {
    switch(action.type){
        case "INCREASE": 
            return state + action.data;
        case "DECREASE":
             return state - action.data;
        default:
             return state;
    }
}

- switch문이 if문보다 가독성있다

 

투두리스트 코드에 적용한 예시

더보기
 import './App.css'
import { useState, useRef, useReducer } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';

 // 투두아이템의 보관형태를 정하기 위한 임시데이터
 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(),
  },
];

// Todos State의 상태변화 담당
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() {
  // 여러 개의 투두테이터를 보관하기 위해 초기값 배열로 설정
  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) => {
    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

최적화

 

 

useMemo와 연산최적화

- 동일한 연산의 결과를 반복적으로 계산하지 않고, 최초 계산 결과를 저장해 재사용하는 기법

// 실습을 위한 불필요한 연산
    const getAnalyzedData = () => {
        const totalCount = todos.length;
        const doneCount = todos.filter((todo)=>todo.isDone).length;
        const notDoneCount = totalCount - doneCount;

        return {
            totalCount,
            doneCount,
            notDoneCount
        }
    };

    const {totalCount, doneCount, notDoneCount} = getAnalyzedData();

- 필터로 인해 수가 많아질수록 연산이 길어진다

- 리렌더링마다 다시 위 연산을 하게 된다(서치바에 검색어만 입력해도 연산 진행)

 const {totalCount, doneCount, notDoneCount} =
        useMemo(() => {
        // memoization하고 싶은 연산을 이 곳에 삽입
        // 리턴값 그대로 반환
        console.log("연산");
        const totalCount = todos.length;
        const doneCount = todos.filter((todo)=>todo.isDone).length;
        const notDoneCount = totalCount - doneCount;

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

- useMemo도 deps에 들어가는 값이 바뀌면 콜백 함수를 다시 실행한다(useEffect와 동일)
- 콜백함수가 반환하는 값을 useMemo는 그대로 다시 반환
- useMemo에 있는 연산은 딱 한 번만 수행되도록 바뀐다
- memo안의 연산이 아예 한 번만 실행되는 것이 아닌, 검색어를 넣을 때에 실행되는 것만 방지하는 것이므로 deps([]안에)에 todos를 입력한다
- useMemo훅을 이용하면 특정조건, deps를 이용한 특정 조건이 만족하지 않을 때에 다시 수행하지 않도록 만들 수 있다


React Memo와 컴포넌트 렌더링 최적화

- 리액트 도구의 설정에서 하이라이트 표시로 리렌더링의 유무를 알 수 있다

import "./Header.css";
import { memo } from "react";

const Header = () => {
    return <div className="Header">
        <h3>오늘은 📅</h3>
        <h1>{new Date().toDateString()}</h1>
    </div>;
};

const memoizedHeader = memo(Header);

export default memoizedHeader;

- memoized를 했기 때문에 헤더에서 불필요한 리렌더링이 발생하지 않는다

 

import { memo } from "react";

export defalut memo(TodoItems);

- 투두아이템이 계속 리렌더링되는데, 이는 props가 바뀌었다고 인식하기 때문이다

(객체의 변수는 값이 같아도 주소에 저장하기 때문에 다른 값으로 인식해서 리렌더링 발생)

 

// 교차 컴포넌트 (HOC)
export default memo(TodoItem, (prevProps, nextProps) => {
    // 반환값에 따라, Props가 바뀌었는지 안바뀌었는지 판단
    // T -> Props 바꾸지 않음 -> 리렌더링 X
    // F -> Props 바뀜 -> 리렌더링 O

    if (prevProps.id !== nextProps.id)  return false;
    if (prevProps.isDone !== nextProps.isDone)  return false;
    if (prevProps.content !== nextProps.content)  return false;
    if (prevProps.date !== nextProps.date)  return false;

    return true;
});

- if문으로 props의 변동을 일일히 다 확인해서

인수로 받은 컴포넌트의 props가 변경되지 않았을 때에는 리렌더링하지 않도록 최적화해서 반환한다
- HOC(Higher Order Component)를 한 번 호출하는 것만으로도 컴포넌트에 새로운 기능을 부여할 수 있다


useCallback과 함수 재생성 방지

- 메모 메서드는 컴포넌트의 props가 바뀌었는지의 유무를 얕은 비교로 판단한다
>> 객체 타입의 값을 props로 전달하면 제대로된 최적화가 이루어지지 않는다

  const onDelete = useCallback(()=>{}, [])

- callback 함수의 첫번째 인수로 재생성 방지를 원하는 함수를 넣고, 두번째 인수로는 depth를 넣는다
- useCallback은 첫번째 인수callback를 그대로 생성해서 반환한다(변수에 저장 가능)
- 생성되는 함수를 depth가 변경되었을 때만 다시 생성되도록 한다 (=함수의 메모이제이션)

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

- 마운트 시에만 함수가 생성된다(리렌더링이 발생해도 함수 재생성이 되지 않음)

 

1. 기능구현 - 2. 최적화 순서로 진행되어야 함
최적화가 필요한 것만(=복잡한 것만) 최적화하기


Context

App에서 ChildB로 전달 불가

- Context는 보통 컴포넌트 외부에 생성
(리렌더링마다 다시 생성될 필요가 없기 때문)
- provider는 컨텍스트가 공급할 데이터를 설정하거나 데이터를 공급받을 컴포넌트를 설정하기 위해 사용하는 props
>> 사실상 컴포넌트

 

const TodoContext = createContext();


 return (
    <div className="App">
      <Header />
      <TodoContext.Provider 
       value={
          {todos,
         onCreate,
         onDelete,
       }}
      >
      <Editor onCreate={onCreate}/>
      <List 
      todos={todos} 
      onUpdate={onUpdate}
      onDelete={onDelete}
      />
      </TodoContext.Provider>
    </div>
  );
}

- 프로바이더 컴포넌트 아래에 있는 모든 컴포넌트들은 투두 컨텍스트의 데이터를 공급받을 수 있다
- value에 공급할 데이터를 설정

 

 

 return (
    <div className="App">
      <Header />
      <TodoContext.Provider 
       value={
          {todos,
         onCreate,
         onDelete,
       }}
      >
      <Editor />
      <List />
      </TodoContext.Provider>
    </div>
  );

- props 제거

import {useState, useRef, useContext} from "react";
import { TodoContext } from "../App";

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

- useContext은 context로부터 공급된 데이터를 반환해주는 함수

 

하지만 위의 과정을 실행하면 최적화가 풀리게 되는 문제가 발생한다

>> context를 분리하여 문제 해결

 

- todos의 값이 변경되면 모든 컴포넌트가 리렌더링되는 문제가 발생한다

- 이 문제를 해결하기 위해 컴포넌트를 분리한다

import {
  useRef,
  useState,
  useReducer,
  useCallback,
  createContext,
  useMemo,
} from "react";

export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();

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