본문 바로가기
웹 프로그래밍/Next.js

[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 8 ~ 섹션 9)

by 청량리 물냉면 2024. 1. 13.
반응형

섹션 8: useReducer로 상태관리 로직 분리하기

useReducer

useState와의 공통점

  • 새로운 state 생성
  • State를 업데이트시키는 함수 제공

 

useState vs useReducer

  • useState: 컴포넌트 내부에 State 관리 로직을 작성해야 함 → 컴포넌트 내부에 들어갈 로직이 복잡해지고 길어질 수록 가독성이 매우 안 좋아짐
  • useReducer: 컴포넌트 외부에 State 관리 로직 분리 가능 컴포넌트 코드를 간결하게 유지 가능 (복잡한 상태관리 로직이 사용될 때 합리적)

 

Reducer 사용X

import { useState } from "react";

export default function A() {
  const [count, setCount] = useState(0);

  const onDecrease = () => {
    setCount(count - 1);
  };

  const onIncrease = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h4>{count}</h4>
      <button onClick={onDecrease}>-</button>
      <button onClick={onIncrease}>+</button>
    </div>
  );
}

👇

Reducer 사용

import { useReducer } from "react";

//컴포넌트 외에 로직 작성
function reducer(state, action) {
  if (action.type === "DECREASE") {
    return state - action.data;
  } else if (action.type === "INCREASE") {
    return state + action.data;
  }
}

export default function B() {
  const [count, dispatch] = useReducer(reducer, 0);
  //count: state의 값
  //dispatch: 상태변화를 발동시키는 트리거 역할을 하는 함수
  //reducer: 상태변화를 처리하는 함수. 함수가 리턴될 시 count 값의 상태가 변화한다.
  //0: 초기값

  return (
    <div>
      <h4>{count}</h4>
      <button
        onClick={() => {
          dispatch({ type: "DECREASE", data: 1 }); //action 객체
        }}
      >
        -
      </button>
      <button
        onClick={() => {
          dispatch({ type: "INCREASE", data: 1 }); //action 객체
        }}
      >
        +
      </button>
    </div>
  );
}

 

 

투두리스트 업그레이드(useReducer)

Reducer 사용X

import { useState, useRef } from "react";
...

const mockData = [
  {
    id: 0,
    isDone: true,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  ...
];

function App() {
  const [todos, setTodos] = useState(mockData);
  const idRef = useRef(3);

  const onCreate = (content) => {
    //content: input 태그에 사용자가 입력한 투두 항목
    const newTodo = {
      id: idRef.current++,
      isDone: false,
      content: content, //content로만 작성해도 가능(js 특성)
      createdDate: new Date().getTime(),
    }; //객체로 만드는 이유: 그렇게 설계해놨기 때문(mockData)

    setTodos([...todos, newTodo]); //기존 배열+새로운 요소 추가
  };

  const onUpdate = (targetId) => {
    setTodos(
      todos.map((todo) =>
        todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
      )
    );
  };

  const onDelete = (targetId) => {
    setTodos(todos.filter((todo) => todo.id !== targetId));
  };

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App; 

👇

Reducer 사용

import { useRef, useReducer } from "react";
...

const mockData = [...];

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

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

  const onCreate = (content) => {
    //setTodo 대신 dispatch
    dispatch({
      //action객체
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content, //content로만 작성해도 가능(js 특성)
        createdDate: new Date().getTime(),
      }, //객체로 만드는 이유: 그렇게 설계해놨기 때문(mockData)
    });
  };

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

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

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App;

 

 

섹션 8: 최적화

투두리스트 최적화 (useMemo: 재연산 방지하기)

...

export default function TodoList({ todos, onUpdate, onDelete }) {
  ...
  
  const getAnalyzedTodoData = () => {
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  };

  //구조분해 할당으로 받아오기
  const { totalCount, doneCount, notDoneCount } = getAnalyzedTodoData();

  return (
    <div className="TodoList">
      <h4>Todos</h4>
      <div>
        <div>전체 todo: {totalCount}</div>
        <div>완료 todo: {doneCount}</div>
        <div>미완 todo: {notDoneCount}</div>
      </div>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요"
      />
      ...
    </div>
  );
}
  • filter 함수를 사용하고 있으므로, 배열의 길이가 길어질 수록 배열 순회 연산량이 길어진다.
  • searchBar에 검색어를 입력할 때마다 분석 함수가 실행된다. (불필요한 연산)

useMemo

특정 조건을 만족하지 않을 시 연산을 다시 수행하지 않도록 하는 리액트 훅. 연산 최적화를 목적으로 한다.

  //구조분해 할당으로 받아오기
  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]); //todos의 값이 바뀔 때마다 실행된다.

불필요한 연산 없이 todos의 값이 바뀔 때마다(새로운 todo가 추가되거나 삭제) 실행된다.

 

 

투두리스트 최적화2 (React.memo: 컴포넌트 리렌더 방지하기)

todo 추가 시 todo와 전혀 상관없는 Header 컴포넌트가 리렌더링 되는 현상이 발생

  • 이유: todo 추가 시 부모 컴포넌트인 App 컴포넌트가 변경되고, 따라서 App 컴포넌트의 자식 컴포넌트인 Header 컴포넌트도 리렌더링된다.
  • 해결 방안: React.memo

React.memo

부모 컴포넌트가 리렌더되더라도 인수로 받은 컴포넌트가 변경되지 않으면 해당 컴포넌트는 리렌더되지 않는다.

header.jsx

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

export default unction Header() {
  return (
    <div className="Header">
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
}

👇

React.memo 사용

header.jsx

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

function Header() {
  return (
    <div className="Header">
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
}

//memo: 인수로 컴포넌트 전달받아서 최적화된 컴포넌트로 반환
const OptimizedHeaderComponent = memo(Header);

export default OptimizedHeaderComponent;

 

<문제점> React.memo와 원시 자료형, 참조 자료형

TodoItem.jsx

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

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

  const onClickDeleteButton = () => {
    onDelete(id);
  };

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

export default memo(TodoItem);

👉 이렇게 React.memo를 지정해 주어도, 투두리스트 하나의 체크박스를 체크했을 때 나머지 아이템들에서도 렌더링이 동시에 일어난다. 이유는 새로운 컴포넌트를 추가하거나 체크박스를 눌러 isDone의 상태를 바꿀 때, 나머지 TodoItem들이 전달받는 props도 모두 바뀌기 때문이다.

 

원시 자료형

  • 변수에 값 자체가 저장됨
  • 숫자, 문자열, 불리언, Null, Undefined

참조 자료형

  • 변수에 참조값(주소값)으로 저장됨
  • 매번 프로젝트가 렌더되어 다시 호출될 때마다 다른 참조값을 가지므로, 렌더링 이전과 동일하지 않다.
  • 객체, 배열, 함수

TodoItem.jsx에서는 TodoItem의 props로 다음과 같은 항목들을 전달받고 있다. ({ id, isDone, createdDate, content, onUpdate, onDelete })

함수 onUpdate, onDelete는 상위 컴포넌트인 App 컴포넌트에서 정의 및 호출되고 있다. 

참조 자료형의 특징 때문에, 값이 고정된  id, isDone, createdDate, content와 달리 함수인 onUpdate, onDelete는 렌더링 될 때마다 참조값이 달라지며, 따라서 렌더링 할 때마다 상태 변화가 일어난다.

따라서 TodoItem.jsx에서 한 아이템의 체크박스를 체크하여 상태 변화가 발생한다면 todos의 상태가 변하게 되고, todos는 App 컴포넌트에 정의되어 있으므로 상태변화를 감지한 App 컴포넌트도 리렌더링된다. App 컴포넌트가 리렌더링될 때마다 각 TodoItem들에게 내려주는 함수도 매번 바뀌게 되므로 나머지 TodoItem들에서도 렌더링이 다시 일어나게 된다.

 

<문제점 해결방안> 투두리스트 최적화3 (useCallback: 함수 재생성 방지하기)

App.jsx

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

const mockData = [...];

function reducer(state, action) {
  ...
}

function App() {
  const [todos, dispatch] = useReducer(reducer, mockData);
  ...

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: "UPDATE",
      data: targetId,
    });
  }, []); //의존성 배열. 배열 안의 내용이 바뀔 때 리렌더링됨

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

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App;

 

 

 

 

출처

https://kmooc.udemy.com/course/react-next-master/learn/lecture/39610746

 

반응형