개발조각

[React] useReducer 본문

React

[React] useReducer

개발조각 2023. 9. 19. 20:23
728x90
반응형

컴포넌트의 state를 생성하고 관리하기 위해서는 useState훅을 사용해 왔는데 리액트에서 state관리를 위한 또 다른 훅이 있다.

그건 바로 useReducer라는 훅이다.

 

useReducer 개념 설명


useReducer는 useState처럼 state를 생성하고 관리할 수 있게 해주는 도구이다.

 

useReducer는 언제 사용하는 걸까?

여러 개 하위값을 포함하고 있는 복잡한 state를 다뤄야 될 때 useState대신에 useReducer를 사용하면

코드를 깔끔하게 쓸 수 있고, 유지보수도 쉬워진다.

// State
{
    teacher: 'James',
    students: ['Kim', 'Ann', 'John'],
    count: 3,
    locations: [
    	{country: 'Korea', name: 'A'},
        {country: 'Australia', name: 'B'},
    ]
}

 

useReducer를 사용하기전 알아야 될 점

useReducer는 Reducer, Dispatch, Action 3가지로 이루어져 있다.

Reducer는 state를 업데이트를 해주는 역할을 한다.

(컴포넌트의 State를 업데이트 시켜주고 싶으면 Reducer를 통해서 해야 된다.)

  • 거래내역 : State
  • 은행 : Reducer
  • 철수의 요구 행위 : Dispatch
  • 만원을 출금해주세요 : Action

철수가 거래내역(State)을 업데이트하기 위해서는 요구(Dispatch)에 "만원을 출금해 주세요."(Action)라는 내용을 담아 은행(Reducer)에 전달하면 은행(Reducer)은 내용(Action) 내용대로 거래내역(State)을 업데이트 해주는 것이다.

철수는 은행(Reducer)에 다른 내용(Action)을 보냄으로써 예금, 출금, 송금 등 복잡한 일을 할 수 있다.

 

위의 내용을 컴포넌트 관점에서  보자면

State를 업데이트 시켜주기 위해 Dispatch함수에 인자로 Action을 넣어서 Reducer에게 전달하면

Reducer가 컴포넌트의 State를 Action안에 들어있는 내용으로 업데이트를 시킨다.

 

예제1 (은행)


  • reducer - state를 업데이트하는 역할 (은행)
  • dispatch - state 업데이트를 위한 요구
  • action - 요구의 내용
const reducer = (state, action) => {};

money state는 이곳에서만 수정 가능

여기서의 reducer는 useReducer의 인자로 전달

 

const [money, dispatch] = useReducer(reducer, 0);

reducer를 통해 money state를 수정할 때마다 dispatch를 불러줄 거고

dispatch는 useReducer가 만들어주는 함수인데 action이라는 인자를 넣어줄 예정

 

const reducer = (state, action) => {
    console.log("reducer가 일을 합니다!", state, action);
};

const UseReducer1 = () => {
    const [number, setNumber] = useState(0);
    const [money, dispatch] = useReducer(reducer, 0); // 0은 여기서 0

    return (
        <div>
            <h2>useReducer 은행에 오신것을 환영합니다.</h2>
            <p>잔고: {money}원</p>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(parseInt(e.target.value))}
                step="1000"
            />
            <button
                onClick={() => {
                    dispatch(); // undefined는 dispatch의 인자로 아무것도 안넣었기 때문
                }}
            >
                예금
            </button>
            <button>출금</button>
        </div>
    );
};

export default App;

예금을 클릭하면 이와 같은 콘솔이 찍힌다.

0은 money state는 0으로 초기값

undefined는 dispatch인자로 아무것도 주지 않았기 때문이다.

 

수정 후 다시 예금 클릭

<button
    onClick={() => {
        dispatch({ type: "deposit", payload: number });
    }}
>

 

function App() {
    return (
        <>
            <UseReducer1 />
        </>
    );
}

const reducer = (state, action) => {
    console.log("reducer가 일을 합니다!", state, action);
    return state + action.payload;
};

const UseReducer1 = () => {
    const [number, setNumber] = useState(0);
    const [money, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h2>useReducer 은행에 오신것을 환영합니다.</h2>
            <p>잔고: {money}원</p>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(parseInt(e.target.value))}
                step="1000"
            />
            <button
                onClick={() => {
                    dispatch({ type: "deposit", payload: number });
                }}
            >
                예금
            </button>
            <button>출금</button>
        </div>
    );
};

export default App;

 useReducer도 useState와 같이 state가 바뀔 때마다 컴포넌트를 렌더링이 되기 때문에 화면에 출력된다.

 

예금 말고도 출금도 해야 되기 때문에 reducer가 payload를 항상 더해주면 안 된다.

action에 있는 type에 따라서 다르게 업데이트를 해줘야 된다.

그래서 reducer안에 if else문, switch문을 많이 쓴다.

const reducer = (state, action) => {
    console.log("reducer가 일을 합니다!", state, action);

    switch (action.type) {
        case "deposit":
            return state + action.payload;
        default:
            return state;
    }
};

useReducer의 장점은 전달받은 action대로만 state를 업데이트시켜준다.

만약 알 수 없는 타입을의 action을 보냈다면 Reducer는 아무 일도 하지 않고 이전의 State를 리턴해준다.

=> 실수를 줄여준다.

 

출금기능도 추가

const reducer = (state, action) => {
    console.log("reducer가 일을 합니다!", state, action);

    switch (action.type) {
        case "deposit":
            return state + action.payload;
        case "withdraw":
            return state - action.payload;
        default:
            return state;
    }
};

const UseReducer1 = () => {
    const [number, setNumber] = useState(0);
    const [money, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h2>useReducer 은행에 오신것을 환영합니다.</h2>
            <p>잔고: {money}원</p>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(parseInt(e.target.value))}
                step="1000"
            />
            <button
                onClick={() => {
                    dispatch({ type: "deposit", payload: number });
                }}
            >
                예금
            </button>
            <button
                onClick={() => {
                    dispatch({ type: "withdraw", payload: number });
                }}
            >
                출금
            </button>
        </div>
    );
};
export default App;

 

코드를 좀 더 깔끔하게 만들고 싶다면

const ACTION_TYPES = {
    deposite: "deposit",
    withdraw: "withdraw",
};

const reducer = (state, action) => {
    console.log("reducer가 일을 합니다!", state, action);

    switch (action.type) {
        case ACTION_TYPES.deposit:
            return state + action.payload;
        case ACTION_TYPES.withdraw:
            return state - action.payload;
        default:
            return state;
    }
};

const UseReducer1 = () => {
    const [number, setNumber] = useState(0);
    const [money, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <h2>useReducer 은행에 오신것을 환영합니다.</h2>
            <p>잔고: {money}원</p>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(parseInt(e.target.value))}
                step="1000"
            />
            <button
                onClick={() => {
                    dispatch({ type: ACTION_TYPES.deposit, payload: number });
                }}
            >
                예금
            </button>
            <button
                onClick={() => {
                    dispatch({ type: ACTION_TYPES.withdraw, payload: number });
                }}
            >
                출금
            </button>
        </div>
    );
};

export default App;

 

예제 2 (출석부 2)


추가 기능 구현 (add-student)

const reducer = (state, action) => {
    console.log(state, action);

    switch (action.type) {
        case "add-student":
            const name = action.payload.name;
            const newStudent = {
                id: Date.now(),
                name,
                isHere: false,
            };
            return {
                count: state.count + 1,
                students: [...state.students, newStudent],
            };

        default:
            return state;
    }
};
const initialState = {
    count: 0,
    students: [],
};

const UseReducer2 = () => {
    const [name, setName] = useState("");
    const [studentsInfo, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <h1>출석부</h1>
            <p>총 학생 수: {studentsInfo.count}</p>
            <input
                type="text"
                placeholder="이름을 입력해주세요"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <button
                onClick={() =>
                    dispatch({ type: "add-student", payload: { name } })
                }
            >
                추가
            </button>

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

const Student = ({ name }) => {
    return (
        <div>
            <span>{name}</span>
            <button>삭제</button>
        </div>
    );
};

export default App;

삭제 기능 구현 (delect-student)

const reducer = (state, action) => {
    console.log(state, action);
    switch (action.type) {
        case "add-student":
            const name = action.payload.name;
            const newStudent = {
                id: Date.now(),
                name,
                isHere: false,
            };
            return {
                count: state.count + 1,
                students: [...state.students, newStudent],
            };

        case "delete-student":
            return {
                count: state.count - 1,
                students: state.students.filter(
                    (student) => student.id !== action.payload.id
                ),
            };

        default:
            return state;
    }
};
const initialState = {
    count: 0,
    students: [],
};

const UseReducer2 = () => {
    const [name, setName] = useState("");
    const [studentsInfo, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <h1>출석부</h1>
            <p>총 학생 수: {studentsInfo.count}</p>
            <input
                type="text"
                placeholder="이름을 입력해주세요"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <button
                onClick={() =>
                    dispatch({ type: "add-student", payload: { name } })
                }
            >
                추가
            </button>

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

const Student = ({ name, dispatch, id }) => {
    return (
        <div>
            <span>{name}</span>
            <button
                onClick={() => {
                    dispatch({ type: "delete-student", payload: { id } });
                }}
            >
                삭제
            </button>
        </div>
    );
};

export default App;

 

출석했는지 안 했는지 기능 구현 (mark-student)

const reducer = (state, action) => {
    console.log(state, action);
    switch (action.type) {
        case "add-student":
            const name = action.payload.name;
            const newStudent = {
                id: Date.now(),
                name,
                isHere: false,
            };
            return {
                count: state.count + 1,
                students: [...state.students, newStudent],
            };

        case "delete-student":
            return {
                count: state.count - 1,
                students: state.students.filter(
                    (student) => student.id !== action.payload.id
                ),
            };

        case "mark-student":
            return {
                count: state.count,
                students: state.students.map((student) => {
                    if (student.id === action.payload.id) { // payload의 id라면
                        return { ...student, isHere: !student.isHere }; // student의 나머지 속성은 같고 isHere만 바꾸기
                    }
                    return student; // 나머지 student는 동일하게
                }),
            };

        default:
            return state;
    }
};
const initialState = {
    count: 0,
    students: [],
};

const UseReducer2 = () => {
    const [name, setName] = useState("");
    const [studentsInfo, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <h1>출석부</h1>
            <p>총 학생 수: {studentsInfo.count}</p>
            <input
                type="text"
                placeholder="이름을 입력해주세요"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <button
                onClick={() =>
                    dispatch({ type: "add-student", payload: { name } })
                }
            >
                추가
            </button>

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

const Student = ({ name, dispatch, id, isHere }) => {
    return (
        <div>
            <span
                style={{
                    textDecoration: isHere ? "line-through" : "none",
                    color: isHere ? "gray" : "black",
                }}
                onClick={() => {
                    dispatch({ type: "mark-student", payload: { id } });
                }}
            >
                {name}
            </span>
            <button
                onClick={() => {
                    dispatch({ type: "delete-student", payload: { id } });
                }}
            >
                삭제
            </button>
        </div>
    );
};

export default App;

 

reducer를 사용했기 때문에 state를 업데이트하는 모든 로직을 reducer안에 담을 수 있게 되었다.

각각의 action이 정해져 있어 reducer 예상한 데로만 state를 변경해 준다.

 

 이 글은 별코딩 리액트 훅 강의를 보고 정리한 글입니다.

728x90
반응형

'React' 카테고리의 다른 글

[React] Custom Hooks  (0) 2023.09.20
[React] React.memo  (0) 2023.09.20
[React] useCallback  (0) 2023.09.19
[React] useMemo  (0) 2023.09.15
[React] useContext + Context API  (0) 2023.09.15
Comments