🍀

useReducer - 백업

5.1 useReducer란?

컴포넌트의 state를 생성하고 관리하기 위해 React에서는 기본 Hook인 useState와 useState로부터 파생된 추가적인 Hook인 useReducer를 제공하고 있습니다. 두 Hook은 같은 역할을 하지만 useState는 사용자가 직접 상태에 접근하는 것이 가능했다면 useReducer는 action 객체와 reducer 함수를 통해 상태에 접근한다는 차이점을 가지고 있습니다.
 
useReducer를 쉽게 이해하기 위해서 그림을 통해 useState와 useReducer의 차이를 살펴보겠습니다.
 
그림 5-1그림 5-1
그림 5-1
 
위 그림은 useState를 은행에 빗대어 설명한 것입니다. ‘입금을 할 것이다’라는 요구사항을 가진 사용자와 사용자의 계좌인 state가 존재합니다. 사용자는 입금을 하기 위해서 직접 계좌에 입금 내역을 작성해야 합니다. 이때 직접 state를 변경하는 것이 setState가 됩니다.
 
그림 5-2그림 5-2
그림 5-2
 
 
위 그림은 useReducer를 표현한 것입니다. 사용자는 위와 같은 요구사항을 가지고 있으며, useState와 달리 은행이라는 중간자 역할이 추가되었습니다. 이때, 사용자는 ‘입금’이라는 action을 은행에 보내면 은행에서 사용자 계좌의 상태를 업데이트하게 됩니다.
 
이렇게 사용자의 요구를 dispatch라고 볼 수 있으며, action에 내용을 담아 reducer의 역할을 하는 은행에 전달하면 계좌의 state를 업데이트할 수 있는 것입니다. useReducer를 활용하면 입금이나 출금 등 다양한 일을 사용자가 직접 처리하지 않고 action으로 간단하게 처리할 수 있기 때문에 복잡한 상태를 다루어야 한다면 useReducer를 사용하는 것이 좋습니다.
 
그림 5-3그림 5-3
그림 5-3
 
useReducer의 흐름에 대해 살펴보겠습니다. state를 업데이트시키기 위해서 dispatch 함수의 인자로 action을 넣어서 reducer에 전달합니다. reducer는 우리가 넣은 action에 맞춰 state를 업데이트하게 됩니다.
 
💡
dispatch, action, reducer 이해하기 dispatch는 state 업데이트를 위한 요구이며, action은 요구의 내용, reducer는 state를 업데이트 해주는 역할이며 컴포넌트의 state를 변경하고 싶다면 reducer를 활용하여 변경하는 것입니다.
 
그렇다면 useReducer와 useState를 어떤 상황에서 어떻게 사용해야 하는지 알아보겠습니다. useReducer는 useState보다 더 복잡한 작업을 처리하는 것이 가능합니다. 상태가 단순할 경우에는 useReducer를 사용하는 것이 코드를 더 복잡하게 만들 수 있기 때문에 useState를 사용하는 것이 적합합니다. 하지만 객체나 배열같이 여러 개의 하위 값을 포함하는 복잡한 상태를 가지거나 확장성이 있을 때는 useReducer를 사용하여 상태 관리를 한다면 코드를 간결하게 해주고 유지보수를 편리하게 할 수 있게 됩니다.

5.1.1. useReducer 기본 구조

const [state, dispatch] = useReducer(reducer, initialArg, init);
 
useReducer의 기본 형태입니다. state는 컴포넌트에서 사용하는 상태, dispatch는 reducer 함수를 실행시키는 함수입니다. useReducer의 첫 번째 인자로는 컴포넌트 외부에서 state 업데이트를 하는 함수인 reducer, 두 번째 인자는 state의 기본값, 세 번째 인자는 선택사항으로 초기함수를 전달합니다.
 
간단한 예제를 통해 useReducer의 사용법을 살펴보겠습니다.
 
import { useState } from "react"; function App() { const [count, setCount] = useState(0); console.log(count); function down() { setCount(count - 1); console.log('사과를 1개 먹었습니다.'); } function reset() { setCount(0); console.log('사과를 모두 먹었습니다.'); } function up() { setCount(count + 1); console.log('사과를 1개 구매했습니다.'); } return ( <div> <p>현재 나에게 있는 사과의 개수는 {count}개</p> <input type="button" value="🍏 1개 먹음!" onClick={down}></input> <input type="button" value="🍎 1개 구매!" onClick={up}></input> <input type="button" value="🍽️ 모두 먹음!" onClick={reset}></input> </div> ); } export default App;
App.jsx
 
그림 5-4그림 5-4
그림 5-4
 
우리에게 익숙한 useState로 증감 기능과 초기화 기능이 있는 간단한 카운터를 구현하였습니다. 각 버튼을 누를 때마다 해당 event에 맞게 count를 업데이트하며 콘솔에 count 값을 출력합니다.
 
import { useReducer } from "react"; function reducer(prevCount, action) { // --- ⓷ if (action === "up") { return prevCount + 1; } else if (action === "down") { return prevCount - 1; } else if (action === "reset") { return 0; } } function App() { const [count, dispatch] = useReducer(reducer, 0); // --- ⓵ // count를 변경하기 위해서는 dispatch를 사용 function down() { // --- ⓶ action 값 전달 dispatch("down"); } function reset() { dispatch("reset"); } function up() { dispatch("up"); } return ( <div> <p>현재 나에게 있는 사과의 개수는 {count}개</p> <input type="button" value="🍏 1개 먹음!" onClick={down}></input> <input type="button" value="🍎 1개 구매!" onClick={up}></input> <input type="button" value="🍽️ 모두 먹음!" onClick={reset}></input> </div> ); } export default App;
App.jsx
 
useState로 구현한 카운터를 useReducer를 사용하여 바꿔보았습니다. ⓵에서 useReducer를 정의합니다. ⓶에서 각 버튼의 이벤트에 대한 onClick 함수를 정의합니다. onClick 이벤트가 발생하면 dispatch는 action 값을 담아 state와 함께 ⓷으로 전달합니다. reducer에서는 전달받은 action과 일치하는 값을 찾는 조건문을 실행하여 새로운 state를 반환합니다.
 
useState를 사용한 카운터에서는 setCount로 직접 state를 변경하였지만, useReducer에서는 setState의 기능을 세분화하여 직접 상태에 접근하지 않으며 상태를 변경하는 것은 reducer 함수에서 집중적으로 처리하게 됩니다. 이러한 방식을 사용하게 되면 reducer 함수 내부에 상태를 변경하는 과정을 은닉할 수 있으며 이것이 useReducer를 사용하는 이유 중 하나입니다.
 
 
 
 

[ 지형님 ]
 

5.3. useReducer 사용해보기

앞서 간단한 예제들을 통하여 useReducer와 reducer에 배운 것들을 이용하여 간단한 로그인 기능을 같이 만들어보도록 하겠습니다.

5.3.1. 예제 설명

아래의 예제는 2장 useState에서 살펴본 useState와 이벤트를 사용한 로그인 폼 예제를 조금 응용한 것이며 이를 useReducer로 변환한 로그인 기능입니다.
 

5.3.2. src 폴더 구조

src 폴더의 구조는 다음과 같습니다.
src ├─ app.css ├─ App.jsx ├─ index.jsx │ ├─ components │ └─ LoginForm.jsx │ ├─ context │ └─ Context.jsx │ └─ reducer └─ Reducer.jsx
 

5.3.3. useState로 구현된 로그인 기능

아래의 코드는 2장 useState에서 살펴본 로그인 폼 예제를 약간 수정한 코드입니다. 아래 useState로 구현된 로그인 폼의 동작 내용을 간단히 살펴보겠습니다.
 
import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; const container = document.getElementById("root"); const root = createRoot(container); root.render(<App />);
index.jsx
 
아래의 App 컴포넌트는 isLogin이라는 로그인 상태가 가진 값에 따라 true이면 “환영합니다~ 라이캣님!” 화면이 나타나고, false이면 로그인 폼이 나타나게 됩니다. 그리고 isLogin의 상태를 변경하는 setIsLogin 함수를 LoginForm 컴포넌트에 props로 전달하여 LoginForm 컴포넌트에서도 isLogin의 상태를 변경할 수 있도록 해줍니다.
import { useState } from "react"; import LoginForm from "./components/LoginForm"; function App() { const [isLogin, setIsLogin] = useState(false); return ( <div> {isLogin ? ( <div> <strong>환영합니다~ 라이캣님!</strong> <img src="https://paullab.co.kr/images/message_licat.png" alt="라이캣" /> <button onClick={() => setIsLogin(!isLogin)}>로그아웃</button> </div> ) : ( <LoginForm setIsLogin={setIsLogin} /> )} </div> ); } export default App;
App.jsx
 
아래의 LoginForm 컴포넌트는 id와 password, 그리고 message를 useState로 선언하고 있습니다. 아이디와 비밀번호는 form 요소 안에 있는 input 요소를 통해 값을 전달 받고 있으며, setId, setPassword를 통해 id와 password 값을 갱신 받고 있습니다.
 
사용자가 input 내용을 모두 입력한 뒤, “로그인 하기” 버튼을 클릭하면 handleLoginForm 함수가 동작하며 id와 password가 모두 일치한 경우에만, App 컴포넌트로부터 전달 받은 setIsLogin 함수를 이용하여 isLogin의 값을 true로 변경하여 줍니다. 그 이외의 경우에는 setMessage 함수를 통하여 “로그인 실패!”라는 문구를 “로그인 하기” 버튼 아래에 표기하여 줍니다.
import { useState } from "react"; function LoginForm({ setIsLogin }) { const [id, setId] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); const handleLoginForm = (event) => { event.preventDefault(); if (id === "licat" && password === "weniv!!") { setIsLogin(true); setMessage("로그인 성공!"); } else { setMessage("로그인 실패!"); } }; return ( <form action="" onSubmit={handleLoginForm}> <label>ID </label> <input type="text" placeholder="아이디를 입력해주세요" onChange={(event) => setId(event.target.value)} /> <br /> <br /> <label>Password </label> <input type="password" placeholder="비밀번호를 입력해주세요" onChange={(event) => setPassword(event.target.value)} /> <br /> <br /> <button>로그인 하기</button> <br /> <p>{message}</p> </form> ); } export default LoginForm;
LoginForm.jsx
 
.main { margin-top: 30px; text-align: center; } img { display: block; width: 400px; margin: 0 auto; height: 400px; }
app.css
 
앞에서 useState를 이용한 간단한 로그인 기능 구현 내용을 살펴보았습니다. 하지만, 조금 더 세부적으로 살펴보면 로그인 과정은 총 4가지 경우의 수로 나누어 생각할 수 있습니다.
 
  1. id와 password가 모두 일치하는 경우 ⇒ 로그인 성공
  1. id만 일치하는 경우 ⇒ 로그인 실패
  1. password만 일치하는 경우 ⇒ 로그인 실패
  1. id와 password 모두 불일치하는 경우 ⇒ 로그인 실패
 
id와 password 중 적어도 하나만 불일치하여도 로그인은 실패하게 됩니다. 하지만, 우리는 조금 더 각각의 상태를 명확히 구분하기 위하여 각각의 경우에 대해 서로 다른 message를 반환하여 주도록 하겠습니다.
 
아래는 앞서 살펴본 LoginForm 컴포넌트의 handleLoginForm 함수에서 조건문을 추가하여 로그인 과정을 조금 더 세분화한 것입니다.
import { useState } from "react"; function LoginForm({ setIsLogin }) { const [id, setId] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); const handleLoginForm = (event) => { event.preventDefault(); if (id === "licat" && password === "weniv!!") { setIsLogin(true); setMessage("로그인 성공!"); } else if (id === "licat" && password !== "weniv!!") { setMessage("비밀번호를 다시 한번 기억해보세요~"); } else if (id !== "licat" && password === "weniv!!") { setMessage("아이디를 다시 한번 기억해보세요~"); } else { setMessage("아이디랑 비밀번호가 모두 틀렸어요~ ㅠㅠ"); } }; return ( <form action="" onSubmit={handleLoginForm}> <label>ID </label> <input type="text" placeholder="아이디를 입력해주세요" onChange={(event) => setId(event.target.value)} /> <br /> <br /> <label>Password </label> <input type="password" placeholder="비밀번호를 입력해주세요" onChange={(event) => setPassword(event.target.value)} /> <br /> <br /> <button>로그인 하기</button> <br /> <p>{message}</p> </form> ); } export default LoginForm;
LoginForm.jsx
 

5.3.4. useReducer로 구현한 로그인 기능

5.1과 5.2을 통하여 알게된 것들을 토대로 앞선 useState로 구현한 로그인 기능을 useReducer로 바꿔보겠습니다.
 
위에서 App, LoginForm으로 컴포넌트들을 각각의 파일로 분리하였지만, 코드 구현사항을 명확히 파악하기 위해 잠시 App.jsx 파일 안에 모든 컴포넌트의 내용을 넣어 살펴보도록 하겠습니다.
 
가장 먼저 살펴볼 사항은 App 컴포넌트입니다. App 컴포넌트는 LoginForm을 분리하지 않았기 때문에 앞서 살펴본 LoginForm의 익숙한 코드가 보이게 됩니다. 여기서 달라진 점은 userInfo라는 사용자 정보가 담긴 객체와 useReducer Hook이 이용된 점, 그리고 handleLoginForm의 조건문에 dispatch가 추가된 부분입니다. 이제 각각 추가된 새로운 부분들을 살펴보도록 하겠습니다.
 
먼저 userInfo는 사용자 정보인 id와 password를 가진 객체입니다. 보통은 서버로부터 사용자 정보를 받아오지만, 우리의 간단한 예제에서는 받아왔다고 가정하고 진행하기 위해 선언한 내용입니다.
 
다음으로 useReducer Hook은 나중에 살펴볼 reducer 함수를 첫 번째 인자로, 두 번째 인자로 초기 상태를 받았습니다. 초기 상태에서 isLogin은 false, 표기될 message는 빈 문자열로 되어 있습니다. 그리고 상태(state)와 dispatch라는 변수와 함수로 useState와 같이 구조 분해 할당 문법을 이용하여 useReducer Hook을 할당합니다.
 
마지막으로 handleLoginForm 함수의 조건문에서는 input 요소로부터 입력 받은 id와 password가 userInfo의 id와 password와 일치하는지 여부를 판단하여 각각의 경우에 맞추어 dispatch를 이용하여 reducer 함수가 반환해야할 값을 action의 type으로 reducer 함수에게 전달하여 주고 있습니다.
특히나, 이 중 로그인이 성공한 경우 (type이 “LOGIN_SUCCESS” 인 경우)를 살펴보면 dispatch 함수에 payload라는 값을 type 이외에 추가로 전달하여 주고 있음을 확인할 수 있습니다. 여기서 payload는 action과 같이 전달하고자 하는 값 또는 데이터를 담는 그릇과 같습니다.
 
이제 reducer 함수의 내용을 살펴보도록 하겠습니다. reducer 함수는 상태(state)와 행동(action)을 파라미터로 전달 받아 action의 type에 따라 state를 업데이트하여 반환시켜주는 함수입니다. 여기서는 switch 문을 사용하여 사용자의 로그인 상태 및 로그인하기 위한 경우의 수를 모두 action의 type으로 받아 구분하였습니다.
import { useState, useReducer } from "react"; import "./app.css"; const reducer = (state, action) => { switch (action.type) { case "LOGIN_SUCCESS": return { ...state, user: action.payload, isLogin: true, message: "로그인 성공!", }; case "MISS_ID": return { ...state, isLogin: false, message: "아이디를 다시 한번 기억해보세요~", }; case "MISS_PASSWORD": return { ...state, isLogin: false, message: "비밀번호를 다시 한번 기억해보세요~", }; case "LOGIN_FAILURE": return { ...state, isLogin: false, message: "아이디랑 비밀번호가 모두 틀렸어요~ ㅠㅠ", }; case "LOGOUT": return { ...state, isLogin: false, message: "로그아웃!", }; default: return { ...state }; } }; function App() { const [id, setId] = useState(''); const [password, setPassword] = useState(''); const userInfo = { id: "licat", password: "weniv!!" }; const [state, dispatch] = useReducer(reducer, { isLogin: false, message: "" }); const handleLoginForm = (event) => { event.preventDefault(); if (id === userInfo.id && password === userInfo.password) { dispatch({ type: "LOGIN_SUCCESS", payload: userInfo }); } else if (id !== userInfo.id && password === userInfo.password) { dispatch({ type: "MISS_ID" }); } else if (id === userInfo.id && password !== userInfo.password) { dispatch({ type: "MISS_PASSWORD" }); } else { dispatch({ type: "LOGIN_FAILURE" }); } }; return ( <div className="main"> {state.isLogin ? ( <> <strong>환영합니다~ 라이캣님!</strong> <img src="https://paullab.co.kr/images/message_licat.png" alt="라이캣" /> <button onClick={() => dispatch({ type: "LOGOUT" })}>로그아웃</button> </> ) : ( <form action="" onSubmit={handleLoginForm}> <label>ID</label> <input type="text" placeholder="아이디를 입력해주세요" onChange={(event) => setId(event.target.value)} /> <br /> <br /> <label>Password</label> <input type="password" placeholder="비밀번호를 입력해주세요" onChange={(event) => setPassword(event.target.value)} /> <br /> <br /> <button>로그인 하기</button> <br /> <p>{state.message}</p> </form> )} </div> ); } export default App;
App.jsx
 
이제 위의 코드가 잘 동작하는지 살펴보도록 하겠습니다. 다음은 각각의 경우에 대한 브라우저 화면을 캡쳐한 모습입니다(참고: 입력내용을 보이기 위해 password 입력창인 input 요소의 type을 text로 변환하여 진행하였습니다).
 
  1. 로그인에 성공한 경우 (”LOGIN_SUCCESS”)
notion imagenotion image
notion imagenotion image
  1. id가 틀린 경우 ("MISS_ID")
notion imagenotion image
 
  1. password가 틀린 경우 ("MISS_PASSWORD")
notion imagenotion image
  1. id와 password가 모두 틀린 경우 ("LOGIN_FAILURE")
notion imagenotion image
 
  1. 로그아웃 버튼을 누른 경우 ("LOGOUT")
notion imagenotion image
 

5.3.5. 컴포넌트의 분리, 그리고 props drilling

앞서 5.3.4에서는 useReducer로의 변환 과정과 그 흐름을 명확히 파악하기 위하여 App.jsx 하나의 파일에 모든 내용이 작성되어 있었습니다. 하지만, 실제로 기능을 구현하는 과정에서는 다양한 컴포넌트들이 서로 분리되어집니다. 그리고 이로 인해 useState에 마주하였던, props drilling 현상을 다시 마주하게 됩니다. 이는 useReducer 이용 시, 다른 컴포넌트에게 state와 dispatch를 전달하려면 불가피하게 나타나는 일입니다.
 
아래는 App.jsx에서 작성하였던 5.3.4의 코드를 컴포넌트 별로 분리한 것입니다.
 
const Reducer = (state, action) => { switch (action.type) { case "LOGIN_SUCCESS": return { ...state, user: action.payload, isLogin: true, message: "로그인 성공!", }; case "MISS_ID": return { ...state, isLogin: false, message: "아이디를 다시 한번 기억해보세요~", }; case "MISS_PASSWORD": return { ...state, isLogin: false, message: "비밀번호를 다시 한번 기억해보세요~", }; case "LOGIN_FAILURE": return { ...state, isLogin: false, message: "아이디랑 비밀번호가 모두 틀렸어요~ ㅠㅠ", }; case "LOGOUT": return { ...state, isLogin: false, message: "로그아웃!", }; default: return { ...state }; } }; export default Reducer;
Reducer.jsx
 
import { useReducer } from "react"; import Reducer from "./reducer/Reducer"; import LoginForm from "./components/LoginForm"; import "./styles.css"; function App() { const [state, dispatch] = useReducer(Reducer, { isLogin: false, message: "" }); return ( <div className="main"> {state.isLogin ? ( <> <strong>환영합니다~ 라이캣님!</strong> <img src="https://paullab.co.kr/images/message_licat.png" alt="라이캣" /> <button onClick={() => dispatch({ type: "LOGOUT" })}>로그아웃</button> </> ) : ( <LoginForm state={state} dispatch={dispatch} /> )} </div> ); } export default App;
App.jsx
 
import { useState } from "react"; function LoginForm({ state, dispatch }) { const [id, setId] = useState(''); const [password, setPassword] = useState(''); const userInfo = { id: "licat", password: "weniv!!" }; const handleLoginForm = (event) => { event.preventDefault(); if (id === userInfo.id && password === userInfo.password) { dispatch({ type: "LOGIN_SUCCESS", payload: userInfo }); } else if (id !== userInfo.id && password === userInfo.password) { dispatch({ type: "MISS_ID" }); } else if (id === userInfo.id && password !== userInfo.password) { dispatch({ type: "MISS_PASSWORD" }); } else { dispatch({ type: "LOGIN_FAILURE" }); } }; return ( <form action="" onSubmit={handleLoginForm}> <label>ID</label> <input type="text" placeholder="아이디를 입력해주세요" onChange={(event) => setId(event.target.value)} /> <br /> <br /> <label>Password</label> <input type="password" placeholder="비밀번호를 입력해주세요" onChange={(event) => setPassword(event.target.value)} /> <br /> <br /> <button>로그인 하기</button> <br /> <p>{state.message}</p> </form> ); } export default LoginForm;
LoginForm.jsx
 
위와 같이 파일을 분리하여도 LoginForm 컴포넌트에서 App 컴포넌트의 state와 dispatch를 props로 전달 받았기 때문에 App.jsx 파일 하나에서 작성한 것과 동일하게 작동하고 있음을 확인하실 수 있습니다. 하지만, 폴더 구조가 복잡해지고 부모와 자식 컴포넌트 간의 관계가 깊어질수록 또 다시 props drilling의 문제는 해결해야할 과제가 되어집니다. 때문에 useReducer는 useContext와 함께 사용하면 더 좋은 효율적으로 활용할 수 있게 됩니다. 이 부분은 이어지는 5.4 useReducer와 useContext 함께 사용하기에서 보다 자세히 살펴보도록 하겠습니다.