본문 바로가기
카테고리 없음

React hooks 정리

by 개미가되고싶은사람 2025. 9. 5.

들어가기 전

Hooks가 React 생태계에 도입되기 이전, React 컴포넌트는 상태(state)와 라이프사이클(lifecycle) 기능을 활용할 수 있는 Class Component와 그렇지 못한 Functional Component로 나뉘어 있었습니다. 이로 인해 대부분의 복잡한 로직은 필연적으로 Class Component를 통해 구현되었으며, 이는 여러 근본적인 한계를 야기했습니다.

 

Hooks 이전의 문제점

1. 재사용 가능한 로직의 구현이 복잡했습니다. 여러 컴포넌트에서 데이터 가져오기, 구독, 인증 등 동일한 상태성 로직을 재사용해야 할 때, 주로 HOC(Higher-Order Components)나 Render Props와 같은 복잡한 패턴에 의존해야 했습니다. 이러한 패턴들은 특정 상황에서는 유용했지만, 많은 경우 컴포넌트를 여러 겹으로 감싸는 'Wrapper Hell'을 초래

 

2. 프롭스 드릴링(Props Drilling) 현상: 프롭스 드릴링은 상위 컴포넌트의 상태(state)를 하위 컴포넌트로 전달해야 할 때, 중간에 위치한 여러 컴포넌트들을 거쳐 불필요하게 프롭스를 전달하는 현상을 의미합니다. 이는 컴포넌트 트리의 깊이가 깊어질수록 심해져, 코드의 가독성과 유지보수성을 크게 떨어뜨렸습니다.

 

React Hooks는 이러한 Class Component의 한계를 해결하기 위해 탄생했습니다. Hooks는 컴포넌트의 상태성 로직을 '컴포넌트로부터 분리'하여 '재사용 가능하게 만드는' 기능을 제시했습니다.

기존 Class Component의 가장 큰 문제는 state와 lifecycle 로직이 특정 컴포넌트 내부에 강하게 결합되어 있어 재사용이나 분리가 어렵다는 점이었습니다. Hooks는 이 문제를 해결하기 위해 useState나 useEffect와 같은 Hook 함수를 통해 상태나 부수 효과와 관련된 로직을 컴포넌트로부터 독립적으로 분리할 수 있는 메커니즘을 제공합니다.

또한, 프롭스 드릴링 문제를 해결하기 위해 Context API와 함께 useContext 훅을 제공합니다. useContext를 사용하면 컴포넌트 트리의 깊이에 상관없이 전역적인 데이터에 직접 접근할 수 있어 중간 컴포넌트들이 불필요한 프롭스를 통과시키는 것을 막아줍니다.

 

React Hooks의 특징

  • 'use'라는 접두사: 모든 React Hooks는 'use'라는 공통된 단어가 포함되어 있습니다. 이를 통해 커스텀 Hooks를 만들 수도 있습니다.
  • 호출 규칙: React Hooks는 함수형 컴포넌트의 최상위 레벨에서만 호출할 수 있습니다. 조건문이나 반복문 내부에서는 호출할 수 없습니다.

React는 컴포넌트가 렌더링될 때마다 Hooks를 호출하는 순서에 의존하여 상태를 관리합니다. 조건문이나 반복문 안에서 Hooks를 호출하게 되면, 특정 조건에 따라 Hooks의 순서가 엉망이 되어서 오류가 발생합니다. 여기에  예시 추가해줘 코드 예시 말고 

if (isMatch) {
      const [input2, setInput2] = useState() // 오류 발생
}

 

 

React 컴포넌트의 라이프 사이클

  • mount (마운트): 컴포넌트가 처음으로 화면에 나타나는 순간 (초기 렌더링).
  • update (업데이트): 컴포넌트가 다시 렌더링되는 순간 - 상태(state)나 속성(props)이 변경될 때 발생(리렌더링)
  • unmount (언마운트): 컴포넌트가 화면에서 사라지는 순간 (렌더링 제거)

 

 

useState - 상태 관리 

useState는 함수형 컴포넌트에서 상태를 관리할 수 있도록 해주는 가장 기본적이고 핵심적인 React Hook입니다. 이 Hook은 배열을 반환하며, 배열의 첫 번째 요소는 현재 상태 값이고, 두 번째 요소는 상태를 업데이트하는 함수(setter)입니다.  

 

예시 코드

import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0); 

  const onCountChange = (value) => {
    // setCount 함수를 호출하여 현재 'count' 값에 'value'를 더해 상태를 업데이트
    setCount(count + value);
  };

  return (
    <div>
      <h1>현재 카운트: {count}</h1> 

      <div>
        <button onClick={() => onCountChange(1)}>+1</button>
        <button onClick={() => onCountChange(-1)}>-1</button>
      </div>
    </div>
  );
}

useState가 반환하는 setState(setCount) 함수를 통해 상태가 변경되면, React는 이 변화를 감지하고 해당 컴포넌트를 자동으로 다시 렌더링합니다. 이 과정을 통해 화면의 UI가 최신 상태 값에 맞춰 업데이트되며, 직접 DOM을 조작할 필요 없이 상태만 관리하면 됩니다.

 

 

useEffect

useEffect는 컴포넌트의 렌더링 이후에 발생하는 부수 효과(Side Effect)를 관리하는 Hook입니다.

 

useEffect의 가장 중요한 특징은 두 번째 인자인 의존성 배열(deps)입니다. 이 배열은 언제 부수 효과useEffect() 콜백 함수를 다시 실행할지 제어하는 핵심적인 역할을 합니다. 의존성 배열의 사용 방식에 따라 useEffect의 동작이 달라집니다.

의존성 배열 상태 동작 방식 용도 및 주의점
useEffect(() => {
console.log('mount')
}, []) // 빈 배열
컴포넌트가 처음 마운트(Mount)될 때만 한 번 실행 useEffect 콜백 함수의 실행 시점은 의존성 배열에 의해서 정해지는데 빈 배열을 넣으면 초기에만 실행되고 배열의 값이 변경될리 없으니 다시는 실행이 안됩니다.
  useEffect(() => {
  
    if (!isMount.current) {
      isMount.current = true
      return
    }
    console.log('update')
  }) // 인자 생략

컴포넌트가 리렌더링 될 때 실행

조건절이 없으면 마운트, 업데이트 시 둘다 동작합니다. 조건절을 사용함으로써 마운트와 업데이트를 분리할 수 있습니다. - isMount은 useRef를 사용한 값
useEffect(() => {
    // 클린업, 정리함수
    return () => {
      console.log('unmount')
    }
  }, [])
컴포넌트가 화면에서 제거(unmount)될 때, useEffect 함수가 반환하는 클린업 함수가 자동으로 실행됩니다.  
useEffect(() => {
    console.log(`count: ${count}`)
  }, [count]) // deps에 배열 추가
 
배열 내의 종속 변수(dependency) 중 하나라도 값이 변경될 때마다 실행  

 

 

클린업 함수

클린업 함수는 useEffect 훅에 return으로 반환하는 특별한 함수를 말합니다. 이 함수는 useEffect가 시작한 작업을 마무리하고, 컴포넌트가 더 이상 필요 없을 때(unmount) 불필요한 자원을 정리하는 역할을 합니다.

 

 

클린업 함수의 동작 시점

클린업 함수는 useEffect가 실행하는 부수 효과를 "뒷정리"하는 역할을 하므로, useEffect가 다시 실행되거나 컴포넌트가 사라질 때 호출됩니다.

  • 컴포넌트가 화면에서 사라질 때(Unmount): useEffect가 실행된 후 컴포넌트가 화면에서 완전히 제거될 때, react에서 클린업 함수가 호출되어 마지막으로 자원을 정리합니다. 예를 들어, return () => { console.log('unmount') }와 같은 코드는 컴포넌트가 화면에서 사라질 때 unmount 메시지를 출력하게 됩니다.
  • 의존성 배열의 값이 변경될 때: [count]와 같이 의존성 배열이 있는 useEffect의 경우, count 값이 변경되면 React는 새로운 useEffect를 실행하기 전에 이전 useEffect의 클린업 함수를 먼저 실행합니다. 이렇게 이전의 부수 효과를 깨끗하게 정리한 뒤, 새로운 효과를 실행합니다.

 

왜 클린업 함수를 사용해야 하나요?

클린업 함수를 사용하는 가장 중요한 이유는 바로 메모리 누수(memory leak)를 방지하기 위해서입니다. 메모리 누수는 더 이상 필요 없는 데이터나 동작이 메모리에서 해제되지 않고 계속 남아있는 현상으로, 앱의 성능을 저하시킵니다.

예를 들어, setTimeout이나 setInterval로 설정한 타이머를 컴포넌트가 사라지기 전에 취소하지 않으면, 컴포넌트는 사라졌는데도 타이머는 계속 작동합니다. 그리고  외부 API에 대한 구독을 시작했을 때, 컴포넌트가 사라지면 구독을 해지해야 불필요한 네트워크 연결을 막을 수 있습니다.

 

 

useReducer - 로직 분리

useState는 간단한 상태 관리에 매우 효율적이지만, useState 관련 로직이 복잡해지거나 여러 상태가 서로 연관될 경우 몇 가지 한계점이 있습니다. 상태 업데이트 로직을 컴포넌트 내부에 직접 작성해야 하므로 코드의 가독성을 떨어뜨리고, 컴포넌트가 리렌더링될 때마다 내부 코드를 다시 실행합니다. 이로 인해 useState 관련 로직이 복잡해질수록 가독성과 성능이 저하될 수 있습니다. useReducer는 이러한 문제를 해결하기 위해 도입되었으며, 복잡한 상태 업데이트 로직을 컴포넌트와 분리하여 더 효율적으로 관리할 수 있게 해줍니다.

 

코드예시

import { useReducer } from 'react'

// 상태를 실제로 변화시키는 함수
// @param state: 현재 state 값
// @param action: 요청의 내용이 담긴 액션 객체
function reducer(state, action) {
  console.log(state, action)

  switch (action.type) {
    case 'INCREASE':
      return state + action.data
    case 'DECREASE':
      return state - action.data
    default:
      return state
  }
}

const ReducerExam = () => {
  // dispatch은 상태 변화가 있어야 한다는 사실을 알리는 함수 및 발송하는 함수
  const [state, dispatch] = useReducer(reducer, 0) // 인수로 reducer 함수, 초기값

  const onClickPlus = () => {
    // 인수로 상태가 어떻게 변하는지 작성, 해당 함수를 호출하면 reducer 함수가 호출
    // dispatch 인수로 전달되는 객체는 액션 객체라고 부릅니다
    dispatch({
      type: 'INCREASE', 
      data: 1, 
    })
  }

  const onClickMinus = () => {
    dispatch({
      type: 'DECREASE', 
      data: 1, 
    })
  }
  return (
    <div>
      <h1>{state}</h1> <button onClick={onClickPlus}> + </button>
      <button onClick={onClickMinus}> - </button>
    </div>
  )
}

export default ReducerExam

useReducer는 reducer 함수, 초기 상태를 인자로 받아 state와 dispatch 함수를 반환합니다. reducer 함수는 상태를 업데이트하는 순수 로직을 담당하며, dispatch 함수는 action 객체를 reducer에 전달하여 상태 업데이트를 요청합니다. 

 

 

useRef - 값 참조

useRef는 두 가지 주요 용도로 사용되는 Hook입니다. 첫 번째는 특정 DOM 요소에 직접 접근하는 것이고, 두 번째는 컴포넌트의 렌더링 주기와 무관하게 변경 가능한 값을 저장하는 것입니다. useRef는 새로운 래퍼런스(Reference) 객체를 생성하는 기능을 제공하며, 이 객체는 컴포넌트 내부의 변수로서 일반적으로 .current 속성을 통해 그 값을 자유롭게 변경하고 접근할 수 있습니다.

 

첫 번째 용도는 React의 선언적 패러다임과 명령형 패러다임 사이의 가교 역할을 합니다. React는 상태를 변경하면 UI가 자동으로 업데이트되는 선언적 방식으로 동작하지만, 실제 웹 개발에서는 특정 입력 필드에 자동으로 포커스를 주거나, 비디오를 제어하는 등 명령형 작업이 필요한 경우가 있습니다. useRef는 이럴 때 특정 DOM 요소에 대한 참조를 제공하여, React의 효율적인 렌더링 시스템을 유지하면서도 필요한 시점에 직접적인 DOM 조작을 가능하게 합니다.  

 

두 번째 용도는 렌더링을 유발하지 않는 값의 저장입니다. useRef로 생성된 객체는 current 속성에 값을 저장하며, 이 값이 변경되어도 컴포넌트의 리렌더링을 유발하지 않습니다. 이는 이전 state 값을 저장하거나, 컴포넌트의 렌더링 횟수를 세는 등 UI와 무관한 데이터를 다룰 때 유용합니다.

 

useState와 useRef의 명확한 차이점

  • useState는 상태가 변경되면 리렌더링 발생
  • useRef는 current 값이 변경되어도 리렌더링을 유발하지 않습니다.

코드 예시

import { useRef, useState } from 'react'

const App = () => {
  const [input, setInput] = useState({
    name: '',
    birth: '',
    county: '',
    bio: '',
  })

  const refObj = useRef(0)
  console.log('(리)렌더링 시작')
  console.log(refObj)

  // 회원 가입 폼 수정 횟수
  // 그냥 자바스크립트 변수를 사용하면 count의 값이 1이 고정됩니다.
  // 왜냐하면 이벤트 핸들러 호출하면 useState가 실행되어 리랜더링 됩니다.
  // 그렇게 되면 count가 다시 0으로 초기화됩니다
  // useRef는 리렌더링 되어도 값이 초기화 되지 않음
  // count 변수를 전역 변수로 선언하면 컴포넌트가 하나인 경우 아무런 문제가 되지 않지면 컴포넌트가 여러 개면 전역 변수도 공유하기 때문에 오류가 발생
  const countRef = useRef()

  const inputRef = useRef()

  // 통합 이벤트 핸들러
  const onChange = (e) => {
    countRef.current++
    console.log(countRef)
    setInput({
      ...input, // spread 연산자를 사용하지 않으면 기존의 존재하는 프로퍼티가 삭제됩니다.
      [e.target.name]: e.target.value, // 자바스크립트에서 key 자리에 []를 열고 변수를 작성하면 변수의 그 값이 key가 됩니다. (Computed Property Names 문법)
    })
  }

  const onSubmit = () => {
    if (input.name === '') {
      // 이름을 입력하는 DOM 요소 포커스(해당 요소를 선택된 상태로 만드는 것)
      console.log(inputRef.current)
      inputRef.current.focus()
    }
  }

  return (
    <div>
      <button
        onClick={() => {
          refObj.current++
          console.log(refObj.current)
        }}
      >
        useRef++
      </button>
      <div>
        <input
          ref={inputRef}
          name="name"
          value={input.name}
          onChange={onChange}
          placeholder={'이름'}
        />
        {input.name}
      </div>
      <div>
        <input
          name="birth"
          value={input.birth}
          type="date"
          onChange={onChange}
        />{' '}
        {input.birth}
      </div>

      <div>
        <select name="country" onChange={onChange} value={input.county}>
          {/* seclect는 맨 위에 option을 초기 값으로 가짐 */}
          <option></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="jp">일본</option>
        </select>
        {input.county}
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />{' '}
        {input.bio}
      </div>
      <button onClick={onSubmit}>제출</button>
    </div>
  )
}

 

 

useMemo, useCallback, React.memo - 최적화

useMemo와 useCallback은 메모이제이션(Memoization) 기법을 기반으로 불필요한 연산 및 함수 재생성을 방지하여 성능을 최적화하는 데 사용됩니다. 메모이제이션이란, 동일한 연산의 결과값을 메모리에 저장해두고, 동일한 입력이 다시 들어왔을 때 재계산하지 않고 저장된 값을 즉시 반환하는 기법을 의미합니다.  

  • useMemo의 역할: 복잡한 연산의 결과값을 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 연산을 다시 수행하여 불필요한 연산을 방지합니다.  
  • useCallback의 역할: 함수 자체를 메모이제이션하여, 컴포넌트가 리렌더링될 때마다 함수가 새롭게 재생성되는 것을 방지합니다.  

이 두 Hook은 React.memo와 함께 사용될 때 진정한 효과를 발휘합니다. React.memo는 컴포넌트의 props가 변경되지 않았다면, 리렌더링을 건너뛰고 이전에 렌더링된 결과를 재사용하는 컴포넌트입니다. React는 props의 변경 여부를 얕은 비교(Shallow Comparison)를 통해 판단합니다. 함수나 객체와 같은 참조 타입의 경우, 내용이 동일하더라도 리렌더링 시 메모리 주소가 변경되면 다른 값으로 간주하고 자식 컴포넌트를 불필요하게 다시 렌더링합니다.  

 

 

useCallback은 함수 참조 값을 고정시켜 이러한 불필요한 재생성을 막아 React.memo로 감싸진 자식 컴포넌트의 리렌더링을 방지할 수 있습니다.

 

useMemo, useCallback, React.memo 비교

  역할 대상 사용 목적
useMemo 복잡한 연산 결과값 메모라이징 값(Value) 불필요한 연산을 최적화하여 값의 참조 동일성을 유지
useCallback 함수 정의 메모라이징 함수(Function) 불필요한 함수 재생성을 방지하여 함수 참조의 동일성을 유지
React.memo 컴포넌트 감싸기 컴포넌트(Component) props가 변경되지 않았을 때 불필요한 컴포넌트의 리렌더링을 방지

 

 

컴포넌트 구조 - 아래 코드 예시도 포함

한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지

 

코드 예시

useMemo-memo-useCallback.zip
16.84MB

 

 

useContext - Props Drilling 문제 해결

기존의 props 전달 방식은 부모 컴포넌트에서 멀리 떨어진 자식 컴포넌트로 데이터를 전달할 때, 중간에 있는 여러 컴포넌트를 거쳐야 하는 문제가 발생했습니다. 이를 'Props Drilling'이라고 부르며, useContext를 통해서 문제를 해결할 수 있습니다. 

App -> List -> TodoItem

App 컴포넌트가 TodoItem에 필요한 onDelete 함수를 전달하려면, 중간에 있는 List 컴포넌트가 자신에게는 필요 없는 props를 단순히 통과시켜야 합니다.

useContext는 이러한 Props Drilling 문제를 해결하기 위해 도입된 Hook입니다. 컴포넌트 트리 내에서 전역적으로 데이터를 공유할 수 있게 해주며, 중간 컴포넌트를 거치지 않고 원하는 곳에서 Context 값을 바로 사용할 수 있습니다.

 

 

Context API의 구성 요소

  1. React.createContext(): 공유할 데이터를 담을 Context 객체를 생성
  2. Context.Provider: Context 값을 전달하는 공급자 컴포넌트입니다. value prop을 통해 하위 컴포넌트에게 공유할 값을 전달합니다. 이 컴포넌트의 하위에 있는 모든 컴포넌트는 Context 값을 읽을 수 있습니다.  
  3. useContext(Context): 하위 컴포넌트에서 Context 값을 구독하고 읽어오는 Hook입니다.  

 

코드 예시 - App.jsx

import {
  useState,
  useRef,
  useReducer,
  useCallback,
  createContext,
  useMemo,
} from 'react'
import './App.css'
import Header from './components/Header.jsx'
import List from './components/List.jsx'
import Editor from './components/Editor.jsx'
import ReducerExam from './components/ReducerExam.jsx'

const mockData = [
  {
    id: 0,
    isDone: false,
    content: 'React 공부하기',
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: 'effective 공부하기',
    date: new Date().getTime(),
  },
]

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, setTodos] = useState(mockData)
  const [todos, dispatch] = useReducer(reducer, mockData)
  const idRef = useRef(2)

  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,
    })

    // setTodos(todos.filter((todo) => todo.id !== targetId))
  }


  const onDeleteByUseCallback = useCallback((targetId) => {
    dispatch({
      type: 'DELETE',
      targetId: targetId,
    })
  }, [])

  const onUpdateByUseCallback = useCallback((targetId) => {
    dispatch({
      type: 'UPDATE',
      targetId: targetId,
    })
  }, [])

  /**
   * 문제 상황: App -> List -> TodoItem 컴포넌트로 onUpdate, onDelete 함수를 전달하고 있어서
   * 프롭스 드릴링 현상이 발생하고 있습니다.
   *
   * 문제 해결: createContex hook를 사용하여 Context 객체에 데이터를 보관한 뒤 사용하고 싶은
   * 컴포넌트에서 꺼내 사용
   */

  const memoizedDispatch = useMemo(() => {
    return {
      onCreate,
      onDeleteByUseCallback,
      onUpdateByUseCallback,
    }
  }, [])

  return (
    <div className="App">
      <Header />

      {/* <TodoContext.Provider
        value={{
          todos,
          onCreate,
          onUpdateByUseCallback,
          onDeleteByUseCallback,
        }}
      > */}

      <TodoStateContext.Provider value={todos}>
        {/* TodoDispatchContext value 속성에 그대로 객체를 넣는다면 App 컴포넌트가 리렌더링 될 때 다시 객체가
       생성되어 문제가 발생합니다 그래서 React.memo를 이용해서 객체를 저장해서 value에 전달해야 합니다. */}
        <TodoDispatchContext value={memoizedDispatch}>
          <Editor />
          <List />
        </TodoDispatchContext>
      </TodoStateContext.Provider>

      {/* </TodoContext.Provider> */}
    </div>
  )
}

// 일반적으로 Context는 컴포넌트 외부에 생성합니다. 해당 컴포넌트가 리렌더링 될 때 새로운 Context
// 생성하기 때문입니다. 많은 프로퍼티가 있지만 Provider 라는 프로퍼티만 알아도 사용하는데 문제가 없음
// 사실 상 Provider은 컴포넌트입니다. 그래서 사용할 때도 컴포넌트처럼 사용해야 합니다.
// value 속성에 전달하고 싶은 데이터를 넣으면 됩니다.
export const TodoContext = createContext() // 다른 컴포넌트 파일에서 Context 객체를 import하기 위해서 export 선언
// console.log(TodoContext)

export const TodoStateContext = createContext() // 값이 변하는 Context
export const TodoDispatchContext = createContext() // 값이 변하지 않은 Context

export default App

 

코드 예시 - List.jsx

import './List.css'
import TodoItem from './TodoItem.jsx'
import { useState, useMemo, useContext } from 'react'
import { /**TodoContext*/ TodoStateContext } from '../App.jsx'

const List = (/**{ todos, onUpdate, onDelete }**/) => {
  const [search, setSearch] = useState('')
  const todos = useContext(TodoStateContext) // value에 객체가 아닌 todos 그대로 전달하기 때문에 객체 구조 분할 할당 문법 못씀

  const onChangeSearch = (e) => {
    setSearch(e.target.value)
  }

  const getFilteredDate = () => {
    if (search === '') {
      return todos
    }

    // {}가 있으면 retrun을 작성해야됨
    return todos.filter((todo) => {
      return todo.content.toLowerCase().includes(search.toLowerCase())
    })
  }

  const filteredTodos = getFilteredDate()

  // ----------------------------- 불 필요한 연산 문제 해결 ---------------------------
  // 컴포넌트가 리렌더링되면 또 연산을 실행하기 때문에 불 필요한 연산이 생김
  // todos, search 데이터가 변경되면 리렌더링 됩니다.
  // 이 때 onChangeSearch 함수를 실행할 때 불 필요한 연산이 발생합니다
  const getAnalyzedDate = () => {
    console.log('getAnalyzedDate 함수 호출')
    const totalCount = todos.length
    const doneCount = todos.filter((todo) => todo.isDone).length
    const notDoneCount = totalCount - doneCount

    return {
      totalCount,
      doneCount,
      notDoneCount,
    }
  }

  // const { totalCount, doneCount, notDoneCount } = getAnalyzedDate()

  const { totalCount, doneCount, notDoneCount } = useMemo(() => {
    console.log('useMemo 함수 호출')
    const totalCount = todos.length
    const doneCount = todos.filter((todo) => todo.isDone).length
    const notDoneCount = totalCount - doneCount

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


  return (
    <div className="List">
      <h4>Todo List 🌱</h4>

      <div>
        <div>totalCount: {totalCount}</div>
        <div>doneCount: {doneCount}</div>
        <div>notDoneCount: {notDoneCount}</div>
      </div>

      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력해주세요."
      />
      <div className="todos_wrapper">
        {/* <TodoItem /> */}
        {filteredTodos.map((todo) => {
          // react에서는 컴포넌트를 구분하기 위에서 key 속성을 사용
          return (
            <TodoItem
              key={todo.id}
              {...todo}
              // onUpdate={onUpdate}
              // onDelete={onDelete}
            />
          )
        })}
      </div>
    </div>
  )
}

export default List

 

코드 예시 - TodoItem

import './TodoItem.css'
import { memo, useContext } from 'react'
import { /**TodoContext*/ TodoDispatchContext } from '../App'

const TodoItem = ({
  id,
  content,
  date,
  isDone /** , onUpdate, onDelete */,
}) => {
  // Provider에 제공한 데이터를 꺼내올 때 변수 이름을 다르게 선언 방법

  /**
   * 문제 상황: Context를 사용하니까 의도치 않게 React.memo로 TodoItem 컴포넌트의 리렌더링을
   * 최적화했음에도 불구하고 하나의 TodoItem에 변화가 생기면 다른 모든 TodoItem 컴포넌트들까지 함께
   * 리렌더링되는 문제가 발생
   *
   * 문제 원인: 이 문제는 Context.Provider에 전달된 value 때문입니다. TodoContext의 value에 todos 배열과 같은 객체를 담았는데
   * todos 배열의 내용 중 하나만 변경되어도 부모 컴포넌트인 App이 리렌더링될 때 value에 새로운 객체가 할당됩니다.
   * React.memo는 props의 얕은 비교를 통해 리렌더링 여부를 결정합니다.
   * TodoItem 컴포넌트는 Context로부터 props를 받아오는데 value 객체가 매번 새로 생성되므로
   * props가 변경되었다고 인식하게 됩니다. 이로 인해 React.memo가 제 역할을 하지 못하고
   * 모든 TodoItem이 다시 렌더링됩니다.
   *
   * 해결 방법: Context를 변경될 수 있는 값(todos)과, 변경되지 않은 값(onCreate, onUpdate, onDlete)으로 분리하면 됩니다.
   */
  const { onUpdateByUseCallback: onUpdate, onDeleteByUseCallback: onDelete } =
    useContext(TodoDispatchContext)

  const onChangeCheckbox = () => {
    onUpdate(id)
  }

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

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

// memo 커스텀
// export default memo(TodoItem, (prevProps, nextProps) => {
//   // 직접 전 props 랑 후 props를 비교해서 판단
//   // true 반환 시 props가 바뀌지 않았다고 인식해 리렌더링 되지 않습니다.
//   // false 반환 시 props가 바뀌었다고 인식해 리렌더링 됩니다.

//   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
// })

export default memo(TodoItem)