본문 바로가기

React

React - useReducer Hook에 대해

728x90

useState vs useReducer

언제 useState와 useReducer를 사용해야 할까?

 

useState 사용 시

 

첫 번째 예제 : 하나의 함수에서 여러개의 state들을 업데이트 하는 상황이 있다고 가정해보자.

loading, post, error 3가지 state를 통해 포스트를 불러와야 한다.

 

다음과 같이 버튼을 누르면 데이터를 받아오는 코드를 작성해보자.

 

결과화면

 


 

BEFORE

import { useState } from 'react'

const Test = () => {
    const [loading, setLoading] = useState(false)
    const [post, setPost] = useState({})
    const [error, setError] = useState(false)

    const handleFetch = () => {
        fetch('https://jsonplaceholder.typicode.com/posts/1')
            .then(res => {
                return res.json()
            })
            .then(data => {})
            .catch(err => {})
    }
    return (
        <div>
            <button onClick={handleFetch}>
                {loading ? '로딩중입니다..' : '포스트 불러오기'}
            </button>
            <p>{post?.title}</p>
            <span>{error && '무언가 오류가 발생..!'}</span>
        </div>
    )
}

export default Test

 

AFTER

import { useState } from 'react'

const Test = () => {
    const [loading, setLoading] = useState(false)
    const [post, setPost] = useState({})
    const [error, setError] = useState(false)

    // 변경사항
    const handleFetch = () => {
        setLoading(true);
        setError(false);
        fetch('https://jsonplaceholder.typicode.com/posts/1')
            .then(res => {
                return res.json()
            })
            .then(data => {
                setPost(data);
                setLoading(false);
            })
            .catch(err => {
                setError(true);
                setLoading(false);
            })
    }
    return (
        <div>
            <button onClick={handleFetch}>
                {loading ? '로딩중입니다..' : '포스트 불러오기'}
            </button>
            <p>{post?.title}</p>
            <span>{error && '무언가 오류가 발생..!'}</span>
        </div>
    )
}

export default Test

 

useState를 사용해서 완벽하게 작동시킬 수 있다. 그러나 하나의 함수에 6개의 setState 함수를 사용해야 한다는 문제점이 존재한다.

만약 useState를 더 많이 사용하면 그만큼 코드가 길어질 것이다.

 


 

두 번째 예제 : 이번엔 하나의 state를 사용하는데, 복잡한 state 하나가 존재하며 아래와 같은 내용을 전부 컨트롤해야 한다고 가정해보자.

 

만들어 볼 결과화면

 

  • 제목, 내용, 가격을 설정할 수 있고 
  • 카테고리를 선택하고
  • 태그를 추가하며 태그를 클릭 시 삭제할 수 있고
  • 개수 설정을 할 수 있다. 

 

import React, { useRef, useState } from 'react'

const Form = () => {
    const [product, setProduct] = useState({
        title: '',
        desc: '',
        price: 0,
        category: '',
        tags: [],
        images: {
            sm: '',
            md: '',
            lg: '',
        },
        quantity: 0,
    })

    const handleChange = e => {
        setProduct(prev => ({ ...prev, [e.target.name]: e.target.value }))
    }

    const tagRef = useRef()

    const handleTags = () => {
        const tags = tagRef.current.value.split(',')
        tags.forEach(tag => {
            setProduct(prev => ({ ...prev, tags: [...prev.tags, tag] }))
        })
    }

    const handleRemoveTag = tag => {
        setProduct(prev => ({
            ...prev,
            tags: prev.tags.filter(t => t !== tag),
        }))
    }

    const handleIncrease = () => {
        setProduct(prev => ({ ...prev, quantity: prev.quantity + 1 }))
    }

    const handleDecrease = () => {
        setProduct(prev => ({
            ...prev,
            quantity: prev.quantity - 1,
        }))
    }

    return (
        <div>
            <form className='flex flex-col items-center justify-center h-[500px] gap-5'>
                <input
                    type="text"
                    name="title"
                    onChange={handleChange}
                    placeholder="제목"
                />
                <input
                    type="text"
                    name="desc"
                    onChange={handleChange}
                    placeholder="내용"
                />
                <input
                    type="number"
                    name="price"
                    onChange={handleChange}
                    placeholder="가격"
                />
                <p>카테고리:</p>
                <select name="category" id="category" onChange={handleChange}>
                    <option value="sneakers">신발</option>
                    <option value="tshirts">티셔츠</option>
                    <option value="jeans">바지</option>
                </select>
                <p>Tags:</p>
                <textarea
                    ref={tagRef}
                    placeholder="태그를 콤마(,)로 구분해주세요"
                ></textarea>
                <button type="button" onClick={handleTags}>
                    태그 추가
                </button>
                <div className="tags">
                    {product.tags.map(tag => (
                        <small key={tag} onClick={() => handleRemoveTag(tag)}>
                            {tag}
                        </small>
                    ))}
                </div>
                <div className="quantity">
                    <button type="button" onClick={handleDecrease}>
                        -
                    </button>
                    <span>개수 ({product.quantity})</span>
                    <button type="button" onClick={handleIncrease}>
                        +
                    </button>
                </div>
            </form>
        </div>
    )
}

export default Form

 

이 경우에는 useState를 사용하면 코드가 더욱 복잡해진다. 

모든 값들에 대해 함수를 작성하기 때문에 코드를 알아보기 힘들 뿐만 아니라, 중첩된 배열 또는 오브젝트를 다루기 위해 빈번히 spread function(...)을 사용하기 때문에 실수할 가능성도 높아진다. 

 


 

useState 코드를 useReducer로 대체하기

 

useReducer에는 state(상태)와 action(행동)이 존재한다.

action type 따라 state 값을 변경하여 반환해주게 되며, 값을 전달받아 업데이트 해 줄 수도 있다.(payload)

 

첫 번째 예제의 흐름도

 

아래와 같이 postReducer.js 코드를 작성할 수 있다.

초기값으로 INITIAL_STATE를 세팅하고 action type에 따라 어떤 행동을 할 지 결정해주면 된다.

 

const INITIAL_STATE = {
    loading: false,
    post: {},
    error: false,
}

const postReducer = (state, action) => {
    switch (action.type) {
        case 'FETCH_START':
            return {
                loading: true,
                error: false,
                post: {},
            }
        case 'FETCH_SUCCESS':
            return {
                ...state,
                loading: false,
                post: action.payload,
            }
        case 'FETCH_ERROR':
            return {
                loading: false,
                error: true,
                post: {},
            }
        deafult:
            return state;
    }
}

 

이제 첫 번째 예제의 코드를 useReducer를 사용한 코드로 바꿔보자.

 

import { useReducer } from 'react'

const Test = () => {

    const [state,dispatch] =  useReducer(postReducer, INITIAL_STATE)

    const handleFetch = () => {
        // fetch 시작
        dispatch({type:"FETCH_START"});
        fetch('https://jsonplaceholder.typicode.com/posts/1')
            .then(res => {
                return res.json()
            })
            .then(data => {
                // fetch 성공
                dispatch({type:"FETCH_SUCCESS", payload: data});
            })
            .catch(err => {
                // fetch 에러
                dispatch({type:"FETCH_ERROR"});
            })
    }
    return (
        <div className='w-[100vw] h-[100vh] flex flex-col items-center justify-center'>
            <button onClick={handleFetch} className='p-10 m-10'>
                {state.loading ? '로딩중입니다..' : '포스트 불러오기'}
            </button>
            <p>{state.post?.title}</p>
            <span>{state.error && '무언가 오류가 발생..!'}</span>
        </div>
    )
}

export default Test

 

코드가 좀 더 직관적이고 불필요한 코드의 양이 조금 줄일 수 있었다.

관리하는 state가 많아질수록 더 많은 코드를 줄일 수 있을것이다.

 

 TIP : dispatch 함수 안에서 지정할 type들을 바깥으로 빼놓는 것이 좋다.
실수가 줄어들고, 가끔씩 warning이 발생하기도 하는데 이를 방지해준다. 

 

postActionTypes.js

export const ACTION_TYPES = {
    FETCH_START: 'FETCH_START',
    FETCH_SUCCESS: 'FETCH_SUCCESS',
    FETCH_ERROR: 'FETCH_ERROR',
}

 

reducer를 적용한 함수

    const handleFetch = () => {
        // fetch 시작
        dispatch({ type: ACTION_TYPES.FETCH_START })
        fetch('https://jsonplaceholder.typicode.com/posts/1')
            .then(res => {
                return res.json()
            })
            .then(data => {
                // fetch 성공

                dispatch({ type: ACTION_TYPES.FETCH_SUCCESS, payload: data })
            })
            .catch(err => {
                // fetch 에러
                dispatch({ type: ACTION_TYPES.FETCH_ERROR })
            })
    }

 


이번엔 두 번째 예제를 useReducer를 사용한 코드로 변경해보자.

 

formReducer.js

export const INITIAL_STATE = {
    title: '',
    desc: '',
    price: 0,
    category: '',
    tags: [],
    images: {
        sm: '',
        md: '',
        lg: '',
    },
    quantity: 0,
}

export const formReducer = (state, action) => {
    switch(action.type) {
        case "CHANGE_INPUT":
            return {
                ...state,
                [action.payload.name]: action.payload.value
            };
        case "ADD_TAG":
            return {
                ...state,
                tags: [...state.tags, action.payload]
            };
        case "REMOVE_TAG":
            return {
                ...state,
                tags: state.tags.filter(tag => tag !== action.payload)
            };
        case "INCREASE":
            return {
                ...state,
                quantity: state.quantity + 1,
            };
        case "DECREASE":
            return {
                ...state,
                quantity: state.quantity - 1,
            };
        default:
            return state;
    }
}

 

Form.jsx

import React, { useReducer, useRef } from 'react'
import {formReducer, INITIAL_STATE} from "./formReducer"

const Form = () => {
    const [state, dispatch] = useReducer(formReducer, INITIAL_STATE);
    const tagRef = useRef();

    const handleChange = e => {
        dispatch({type:"CHANGE_INPUT", payload: {
            name: e.target.name, value: e.target.value
        }})
    }

    const handleTags = () => {
        const tags = tagRef.current.value.split(",");
        tags.forEach(tag => {
            dispatch({type: "ADD_TAG", payload: tag});
        })
    }

    return (
        <div>
            <form>
                <input
                    type="text"
                    name="title"
                    onChange={handleChange}
                    placeholder="제목"
                />
                <input
                    type="text"
                    name="desc"
                    onChange={handleChange}
                    placeholder="내용"
                />
                <input
                    type="number"
                    name="price"
                    onChange={handleChange}
                    placeholder="가격"
                />
                <p>카테고리:</p>
                <select name="category" id="category" onChange={handleChange}>
                    <option value="sneakers">신발</option>
                    <option value="tshirts">티셔츠</option>
                    <option value="jeans">바지</option>
                </select>
                <p>Tags:</p>
                <textarea
                    ref={tagRef}
                    placeholder="태그를 콤마(,)로 구분해주세요"
                ></textarea>
                <button type="button" onClick={handleTags}>
                    태그 추가
                </button>
                <div className="tags">
                    {state?.tags.map(tag => (
                        <small key={tag} onClick={() => dispatch({type: "REMOVE_TAG", payload: tag})}>
                            {tag}
                        </small>
                    ))}
                </div>
                <div className="quantity">
                    <button type="button" onClick={() => dispatch({type: "DECREASE"})}>
                        -
                    </button>
                    <span>개수 ({state?.quantity})</span>
                    <button type="button" onClick={() => dispatch({type: "INCREASE"})}>
                        +
                    </button>
                </div>
            </form>
        </div>
    )
}

export default Form

 

코드가 더 깔끔하고 직관적으로 변한것을 볼 수 있다!

728x90