useState
와 useReducer
모두 상태 관리를 위해 사용한다. 그러면 구현이 간단한 useState
를 쓰는 것이 좋지 않을까? 왜 굳이 useReducer
를 써야할까?
useState
는 하나의 상태만 관리한다. 그러나 useReducer
는 여러 상태를 함께 관리한다. 따라서 상태가 서로 dependent 할 경우 useReducer
를 사용하는 것이 좋다.
ex) form의 입력 값과 그 입력 값의 유효성을 모두 상태로 관리해야 하는 경우
useState
는 화살표 함수로 이전 상태를 받아서 현재 상태를 업데이트 해야 최신 상태를 보장할 수 있었다. 반면 useReducer
는 최신 상태를 보장하기 때문에 더 간단히 코드를 작성할 수 있다.
예를 들어 login 이라는 기능을 구현한다고 해보자.
login 과 관련된 상태를 useState
로 관리할 경우, 다음과 같이 login이 성공할 때와 실패할 때 각각의 경우에 대해 상태를 업데이트 하는 로직을 컴포넌트 내에 작성하게 된다.
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, showLoader] = useState(false);
const [error, setError] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const onSubmit = async (e) => {
e.preventDefault();
// 로그인 상태 초기화
setError('');
showLoader(true);
try {
await login({ username, password });
// 로그인 성공
setIsLoggedIn(true);
} catch (error) {
// 로그인 실패
setError('Incorrect username or password!');
showLoader(false);
setUsername('');
setPassword('');
}
};
return; // UI 관련 코드
}
반면, useReducer
를 사용하면 다음과 같이 코드를 작성할 수 있다. 즉, Login 이라는 컴포넌트에는 상태를 어떻게 업데이트 할지에 관한 로직을 없앨 수 있다.
function Login() {
const [state, dispatch] = useReducer(loginReducer, initialState);
const { username, password, isLoading, error, isLoggedIn } = state;
const onSubmit = async (e) => {
e.preventDefault();
// 로그인 상태 초기화
dispatch({ type: 'login' });
try {
await login({ username, password });
// 로그인 성공
dispatch({ type: 'success' });
} catch (error) {
// 로그인 실패
dispatch({ type: 'error' });
}
};
return; // UI 관련 코드
}
리듀서는 별도의 함수로 다음과 같이 구현된다.
function loginReducer(state, action) {
switch (action.type) {
case 'field': {
return {
...state,
[action.fieldName]: action.payload,
};
}
case 'login': {
return {
...state,
error: '',
isLoading: true,
};
}
case 'success': {
return {
...state,
isLoggedIn: true,
isLoading: false,
};
}
case 'error': {
return {
...state,
error: 'Incorrect username or password!',
isLoggedIn: false,
isLoading: false,
username: '',
password: '',
};
}
case 'logOut': {
return {
...state,
isLoggedIn: false,
};
}
default:
return state;
}
}
const initialState = {
username: '',
password: '',
isLoading: false,
error: '',
isLoggedIn: false,
};
React는 선언적 프로그래밍 방식을 따른다. 즉, 어떻게 (how) 가 아니라 무엇 (what) 을 구현할 것인지가 중요하다. 이러한 관점에서 useReducer
는 how 와 what 을 분리하여 더 나은 프로그래밍을 가능하게 한다.
코드로 어떤 차이가 있는지 알아보자.
-1
버튼을 누르면 빼기 1,
+1
버튼을 누르면 더하기 1이 되는 간단한 카운터 예제
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const plusClickHandler = () => {
setCount((prev) => prev + 1);
};
const minusClickHandler = () => {
setCount((prev) => prev - 1);
};
return (
<div className="App">
<h2>{count}</h2>
<button onClick={minusClickHandler}>- 1</button>
<button onClick={plusClickHandler}>+ 1</button>
</div>
);
};
export default Counter;
import React, { useReducer } from "react";
const countReducer = (state, action) => {
switch (action.type) {
case "ADD":
return state + 1;
case "SUB":
return state - 1;
default:
throw new Error("error");
}
};
const Counter = () => {
const [countState, dispatchCount] = useReducer(countReducer, 0);
const plusClickHandler = () => {
dispatchCount({ type: "ADD" });
};
const minusClickHandler = () => {
dispatchCount({ type: "SUB" });
};
return (
<div className="App">
<h2>{countState}</h2>
<button onClick={minusClickHandler}>- 1</button>
<button onClick={plusClickHandler}>+ 1</button>
</div>
);
};
export default Counter;
버튼에 따라 다른 모달창을 띄우는 예제
App.js
import { useState } from "react";
import "./styles.css";
import Modal from "./Modal";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [modalType, setModalType] = useState("");
const [modalContent, setModalContent] = useState("");
const openHandler = (type, content) => {
setIsOpen(true);
setModalType(type);
setModalContent(content);
};
const closeHandler = () => {
setIsOpen(false);
setModalType("");
setModalContent("");
};
return (
<>
<button onClick={() => openHandler("error", "에러 발생")}>
에러 모달창 열기
</button>
<button onClick={() => openHandler("confirm", "확인")}>
확인 모달창 열기
</button>
{isOpen && (
<Modal
title={modalType}
content={modalContent}
closeHandler={closeHandler}
/>
)}
</>
);
}
Modal.js
import React from "react";
import "./styles.css";
const Modal = ({ title, content, closeHandler }) => {
console.log(title, content);
return (
<>
<div className="backdrop" />
<div className="modal">
<h2>{title}</h2>
<div className="content">{content}</div>
<button onClick={closeHandler}>닫기</button>
</div>
</>
);
};
export default Modal;
App.js
import { useReducer } from "react";
import "./styles.css";
import Modal from "./Modal";
const initialModalState = {
isOpen: false,
modalType: "",
modalContent: ""
};
const modalReducer = (state, action) => {
switch (action.type) {
case "OPEN_MODAL":
return {
isOpen: true,
modalType: action.modalType,
modalContent: action.modalContent
};
case "CLOSE_MODAL":
return {
isOpen: false,
modalType: "",
modalContent: ""
};
default:
throw new Error("error");
}
};
export default function App() {
const [modalState, dispatchModal] = useReducer(
modalReducer,
initialModalState
);
const openHandler = (type, content) => {
dispatchModal({
type: "OPEN_MODAL",
modalType: type,
modalContent: content
});
};
const closeHandler = () => {
dispatchModal({ type: "CLOSE_MODAL" });
};
return (
<>
<button onClick={() => openHandler("error", "에러 발생")}>
에러 모달창 열기
</button>
<button onClick={() => openHandler("confirm", "확인")}>
확인 모달창 열기
</button>
{modalState.isOpen && (
<Modal
title={modalState.modalType}
content={modalState.modalContent}
closeHandler={closeHandler}
/>
)}
</>
);
}
Modal.js
import React from "react";
import "./styles.css";
const Modal = ({ title, content, closeHandler }) => {
console.log(title, content);
return (
<>
<div className="backdrop" />
<div className="modal">
<h2>{title}</h2>
<div className="content">{content}</div>
<button onClick={closeHandler}>닫기</button>
</div>
</>
);
};
export default Modal;