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

2024. 1. 13. 13:26·웹 프로그래밍/Next.js

섹션 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

 

저작자표시 비영리 변경금지 (새창열림)

'웹 프로그래밍 > Next.js' 카테고리의 다른 글

[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 15 ~ 16: Next.js 렌더링 2 - SSG, Next.js 렌더링 3 - ISR)  (1) 2024.01.22
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 14: Next.js 렌더링 1 - SSR)  (1) 2024.01.22
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 13: 페이지 라우터)  (0) 2024.01.21
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 12: Next.js 소개)  (0) 2024.01.15
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 10: Context)  (1) 2024.01.13
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 2 ~ 섹션 7)  (0) 2024.01.11
'웹 프로그래밍/Next.js' 카테고리의 다른 글
  • [유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 13: 페이지 라우터)
  • [유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 12: Next.js 소개)
  • [유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 10: Context)
  • [유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 2 ~ 섹션 7)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 프로그래밍
        • Programming
        • C | C++
        • Java
        • Python
      • 웹 프로그래밍
        • HTML | CSS
        • JavaScript | TypeScript
        • React
        • Vue.js
        • Next.js
        • Spring & Spring Boot
        • JSP & Servlet
        • DB
      • 웹 프로젝트
        • 웹 프로젝트
        • 🥨스낵몰
        • 👨‍👨‍👧‍👧소셜 가계부
        • 🌜꿈 일기장
        • 🔮포트폴리오 사이트
        • 🏃‍♂️팀 프로젝트: 일정관리 프로그램
        • 📈팀 프로젝트: AI기반 주식 분석 플랫폼
        • 😺Just Meow It: 고양이의 조언
      • 앱 프로그래밍
        • Flutter
        • Kotlin
      • Problem Solving
        • 백준
        • 프로그래머스
        • SWEA
      • Computer Science
        • 알고리즘
        • 컴퓨터 네트워크
        • 이산수학
      • Developer
        • 후기
        • 자료정리
        • 취업 | 취준
        • 웹개발 교육 프로그램
        • TIL
  • 블로그 메뉴

    • 홈
    • Github
  • 공지사항

    • 프로그래밍 공부 중😊
  • 인기 글

  • 태그

    컴퓨터네트워크
    자바스크립트
    ZeroCho
    파이썬
    SWEA
    클론 프로젝트
    리액트
    bfs
    강의내용정리
    알고리즘
    포트폴리오
    d3
    자바
    spring boot
    mysql
    프로그래머스
    Til
    블로그 제작
    React
    AWS
    백준
    Next.js
    타입스크립트
    구현
    뉴렉처
    공식문서
    플러터
    Jiraynor Programming
    프로젝트
    웹사이트
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
[유데미 | winterlood 이정환] 프로젝트로 배우는 React.js & Next.js 마스터리 클래스 강의 내용 정리 (섹션 8 ~ 섹션 9)
상단으로

티스토리툴바