state를 생성하고 관리할 수 있게 해주는 도구다.
여러 개의 하위값을 포함하는 복잡한 state을 다뤄야할 때 사용할 수 있다.
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에 따라 분류
}
간단한 출석부 하나를 만들었다. 코드를 하나씩 뜯어보자.
// 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>
)
}
props의 변화를 감지해 컴포넌트를 재렌더링하는 React 고차 컴포넌트
리액트에서는 부모 컴포넌트가 렌더링되면 자식 컴포넌트도 렌더링이 된다.
만약 부모에서 내려주는 props의 값은 변하지 않아 자식 컴포넌트까지 렌더링될 필요가 없을 때 React.memo
를 사용할 수 있다.
useMemo, useCallback과 같이 렌더링한 결과를 메모리에 따로 저장하는 Memoization을 사용하기 때문에 적절한 곳에만 사용해 최종적으로 성능을 올릴 수 있다.
memoization 하고싶은 컴포넌트를 memo로 감싸주면 된다.
import {memo} from 'react';
memo(Component)
다음과 같이 부모-자식 컴포넌트가 있다.
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)
이제 부모 컴포넌트가 렌더링 되어도 자식 컴포넌트는 저장된 결과를 가져오기 때문에 새로 렌더링되지 않는다.
그런데 부모가 내려주는 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>
)
}