useReducer, React.memo

Yeji·2023년 9월 4일
0

7. useReducer

7-1. 정의

state를 생성하고 관리할 수 있게 해주는 도구다.

여러 개의 하위값을 포함하는 복잡한 state을 다뤄야할 때 사용할 수 있다.

7-2. 기본

useReducer는 Reducer Dispatch Action 으로 구성되어 있다.

Dispatch로 state을 변경할 것을 요구하고, Action으로 어떤 동작을 수행할 것인지 정해줄 수 있다.
그럼 Reducer는 Action에 따라 업데이트된 state을 반환한다.

// value : state 이름
// initialState : state 초기값
// dispatch : action 호출 요구
const [value, dispatch] = useReducer(reducer, initialState)

const reducer = (state, action) =>{
		// state으로 값에 접근
 		// action type에 따라 분류
	}

7-3. 활용

간단한 출석부 하나를 만들었다. 코드를 하나씩 뜯어보자.

// App.js
import React, { useReducer, useState } from 'react'
import Student from './Student'

const reducer = (state, action) => {
	// 정의할 action
}

// reducer state 초기값
const initialState = {
  count: 0,
  students: [],
}

function App() {
  const [name, setName] = useState('')

  const [studentsInfo, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      <h2>출석부</h2>
      <p>총 학생 수 : </p>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button>추가</button>

      <div>
        {studentsInfo.students.map((student) => (
          <Student key={student.id} student={student} dispatch={dispatch} />
        ))}
      </div>
    </div>
  )
}

export default App

App.js의 자식 컴포넌트 Student.js는 다음과 같다.

import React from 'react'

export default function Student({ student }) {
  return (
    <div>
    // 출석 여부에 따라 style 토글
      <span
        style={{
          textDecoration: student.isHere ? 'line-through' : 'none',
          color: student.isHere ? 'gray' : 'black',
        }}
      >
        {student.name}
      </span>
      <button>삭제</button>
    </div>
  )
}

그럼 state을 변경할 actions를 하나씩 구현해보자.

우선 Actions의 타입을 하나로 관리하기 위해 const 변수를 만들자.

// App.js
export const ACTION_TYPES = {
  addStudent: 'add-student',
  deleteStudent: 'delete-student',
  attendStudent: 'attend-student',
}

먼저 학생 추가다.

dispatch 함수를 호출해 type과 payload를 전달한다.

// App.js
 <button
        onClick={() =>
          dispatch({
            type: ACTION_TYPES.addStudent,
            payload: { name },
          })
        }
      >
        추가
</button>

Reducer에서 switch문을 이용해 type을 분류한다.

payload로 전달받은 학생 이름을 넣어 새로운 학생을 만들고, 이를 반영한 state을 반환한다.

// App.js
const reducer = (state, action) => {
  switch (action.type) {
    // 학생 추가
    case ACTION_TYPES.addStudent:
      const newStudent = {
        id: Date.now(),
        name: action.payload.name,
        isHere: false,
      }
      // 새로 반환할 state
      return {
        count: state.count + 1,
        students: [...state.students, newStudent],
      }
    default:
      return state
  }
}

위와 같은 과정으로 학생 추가, 삭제, 출석을 구현한 코드는 다음과 같다.

// App.js
import React, { useReducer, useState } from 'react'
import Student from './Student'

export const ACTION_TYPES = {
  addStudent: 'add-student',
  deleteStudent: 'delete-student',
  attendStudent: 'attend-student',
}

const reducer = (state, action) => {
  switch (action.type) {
    // 학생 추가
    case ACTION_TYPES.addStudent:
      const newStudent = {
        id: Date.now(),
        name: action.payload.name,
        isHere: false,
      }
      // 새로 반환할 state
      return {
        count: state.count + 1,
        students: [...state.students, newStudent],
      }
    // 학생 삭제
    case ACTION_TYPES.deleteStudent:
      return {
        count: state.count - 1,
        students: state.students.filter(
          (student) => student.id !== action.payload.id
        ),
      }
    // 출석 토글
    case ACTION_TYPES.attendStudent:
      return {
        count: state.count,
        students: state.students.map((student) => {
          if (student.id === action.payload.id) {
            return { ...student, isHere: !student.isHere }
          }
          return student
        }),
      }
    default:
      return state
  }
}

const initialState = {
  count: 0,
  students: [],
}

function App() {
  const [name, setName] = useState('')

  const [studentsInfo, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      <h2>출석부</h2>
      <p>총 학생 수 : </p>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button
        onClick={() =>
          dispatch({
            type: ACTION_TYPES.addStudent,
            payload: { name },
          })
        }
      >
        추가
      </button>

      <div>
        {studentsInfo.students.map((student) => (
          <Student key={student.id} student={student} dispatch={dispatch} />
        ))}
      </div>
    </div>
  )
}

export default App
// Student.js
import React from 'react'
import { ACTION_TYPES } from './App'

export default function Student({ student, dispatch }) {
  return (
    <div>
      <span
        onClick={() =>
          dispatch({
            type: ACTION_TYPES.attendStudent,
            payload: { id: student.id },
          })
        }
        style={{
          textDecoration: student.isHere ? 'line-through' : 'none',
          color: student.isHere ? 'gray' : 'black',
        }}
      >
        {student.name}
      </span>
      <button
        onClick={() =>
          dispatch({
            type: ACTION_TYPES.deleteStudent,
            payload: { id: student.id },
          })
        }
      >
        삭제
      </button>
    </div>
  )
}

8. React.memo

8-1. 정의

props의 변화를 감지해 컴포넌트를 재렌더링하는 React 고차 컴포넌트

리액트에서는 부모 컴포넌트가 렌더링되면 자식 컴포넌트도 렌더링이 된다.

만약 부모에서 내려주는 props의 값은 변하지 않아 자식 컴포넌트까지 렌더링될 필요가 없을 때 React.memo를 사용할 수 있다.

useMemo, useCallback과 같이 렌더링한 결과를 메모리에 따로 저장하는 Memoization을 사용하기 때문에 적절한 곳에만 사용해 최종적으로 성능을 올릴 수 있다.

8-2. 기본

memoization 하고싶은 컴포넌트를 memo로 감싸주면 된다.

import {memo} from 'react';

memo(Component)

8-3. 활용

다음과 같이 부모-자식 컴포넌트가 있다.

useState으로 선언한 부모의 나이가 변경된다면 부모 컴포넌트와 자식 컴포넌트가 재레더링 될 것이다.

문제는 자식 컴포넌트의 내용은 전혀 변하지 않았는데, 계속 렌더링된다는 것이다.

// App.js
import React, { useState } from 'react'
import Children from './Children'

export default function App() {
  const [age, setAge] = useState(0)
  const name = 'Baby Name'

  console.log('Parent Rendering 👨‍👩‍👦')

  return (
    <div>
      <h2>Parents</h2>
      <p>Parents Age : {age}</p>
      <button onClick={() => setAge(age + 1)}>Older</button>
      <Children name={name} />
    </div>
  )
}
import React from 'react'

export default function Children({ name }) {
  console.log('Children Rendering 👶')

  return (
    <div>
      <h3>Children</h3>
      <p>name : {name}</p>
    </div>
  )
}

이를 해결하기 위해 다음과 같이 자식 컴포넌트를 memo로 감싸주자.

import React, { memo } from 'react'

function Children({ name }) {
  console.log('Children Rendering 👶')

  return (
    <div>
      <h3>Children</h3>
      <p>name : {name}</p>
    </div>
  )
}

export default memo(Children)

이제 부모 컴포넌트가 렌더링 되어도 자식 컴포넌트는 저장된 결과를 가져오기 때문에 새로 렌더링되지 않는다.

8-4. Reference Type

그런데 부모가 내려주는 props가 참조 타입의 객체라면 말이 달라진다.

렌더링될 때마다 새로 선언된 메모리의 주소값이 달라지기 때문에 props가 달라졌다고 생각해 memo로 감싸주어도 렌더링이 계속해서 일어난다.

// App.js
import React, { useState } from 'react'
import Children from './Children'

export default function App() {
  const [age, setAge] = useState(0)
  const name = {
    firstName: 'Isabella',
    lastName: 'Jung',
  }

  console.log('Parent Rendering 👨‍👩‍👦')

  return (
    <div>
      <h2>Parents</h2>
      <p>Parents Age : {age}</p>
      <button onClick={() => setAge(age + 1)}>Older</button>
      <Children name={name} />
    </div>
  )
}
// Children.js
import React, { memo } from 'react'

function Children({ name }) {
  // 계속 렌더링 됨
  console.log('Children Rendering 👶')

  return (
    <div>
      <h3>Children</h3>
      <p>firstName : {name.firstName}</p>
      <p>lastName : {name.lastName}</p>
    </div>
  )
}

export default memo(Children)

이를 해결하기 위해서 이전에 배웠던 useMemo를 함께 사용할 수 있다.

// App.js
import React, { useState, useMemo } from 'react'
import Children from './Children'

export default function App() {
  const [age, setAge] = useState(0)
  
  // 첫 렌더링시에 도출된 값 Memoizing
  const name = useMemo(() => {
    return {
      firstName: 'Isabella',
      lastName: 'Jung',
    }
  }, [])

  console.log('Parent Rendering 👨‍👩‍👦')

  return (
    <div>
      <h2>Parents</h2>
      <p>Parents Age : {age}</p>
      <button onClick={() => setAge(age + 1)}>Older</button>
      <Children name={name} />
    </div>
  )
}
profile
채워나가는 과정

0개의 댓글