# Global State - 어플리케이션 전체에서 공유되는 State - **state management** 없이 **Global State**를 구현한다면 해당 **Component**를 받을 때까지 연결되어 있는 모든 페이지에 Pros를 넘겨야 하는 상황이 벌어진다. # Jotai ## Recoil 말고 Jotai을 선택한 이유 - **React JS**에서 사용할 수 있는 **state management library** 중 하나이다. - 하지만 2025년 1월 12일 이후로 업데이트가 중단됐다. - 따라서 Recoil과 같은 Jotai를 사용해보기로 했다. - Jotai은 메우 작은 크기로 돌아가고 가볍게 사용하기 좋아 부담 없이 사용 가능하다. ## Jotai 설치 ```shell npm install jotai ``` ## Jotai 사용법 - jotai의 **atom**은 상태 조각으로 아주 작은 단위의 상태를 의미한다. ```jsx // atom 선언 방법 const valueAtom = atom(10000); const ListAtom = atom<number>([1, 2, 3]) ... ``` - **Atom**은 세 가지 패턴이 있다. - **읽기 전용** : useSetAtom(write) - **쓰기 전용** : useSetAtom(write) - **읽기-쓰기** : useAtom(read/write) - jotai의 상태를 변화 시키기 위해서는 **useAtom**을 사용하게 되는데, 사용 방식이 **useState**와 거의 비슷하다. (읽기-쓰기) ```jsx // useAtom(read/write) const [value, setValue] = useAtom(valueAtom) // useSetAtom(write) const setValue = useSetAtom(valueAtom); // useAtomValue(read) const value = useAtomValue(valueAtom); ``` ## Selector - **get** Parameter를 통해 다른 Atom 값을 가져온 다음 이를 가공해 Return하는 **Selector**를 만들 수 있다. ```jsx export const toDoSelector = atom((get) => { const toDoList = get(toDoState); const category = get(toDoCategoryState); return toDoList.filter((toDo) => toDo.category === category); }); ``` ## React Hook Form - 쉬운 유효성 검사를 통해 성능이 뛰어나고 유연하며 확장 가능한 Form이다. ```bash npm install react-hook-form ``` ## register - **react-hook-form**의 **useForm**의 **register**를 이용하면 **useState**와 **onChange**를 대신하여 사용할 수 있다. - **register** 안에는 **name**과 **onChange, ref** 등이 담겨있다. - **register**의 **String parameter**로 넘기면 해당 값이 name이 된다. - **register**안에 따로 **Option**을 넣어주어 **Validatoin Checking**을 할 수 있다. - value : 해당 Vaildation 기준 값들 - message : 해당 value가 유효하지 않을 때 나타나는 메시지 ```jsx <input {...register("email", { required: true, pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$/i, message: "Invalid email address" }})} type="email" placeholder="Email" /> <input {...register("password", { required: true, minLength: { value: 5, message: "Password must be at least 5 characters" } })} type="password" placeholder="Password" /> ``` - **validation**으로 value값을 받아서 내가 직접 **validation**을 설정할 수 있다. return 값으로 **message**(error message)를 반환할 수 있고 **boolean** 값을 반환할 수도 있다. - **register** 값을 **Input**에 **spread** 해서 넣어주면 onChange와 value값이 자동으로 지정된다. ## watch - **register**의 각 **value**들의 변화를 감지하는 함수이다. ## **handleSubmit** - 두 가지 함수를 안에 넣어 Form의 입력 버튼을 누르면 작동시켜준다. - **onValid** : 해당 Form의 Data들이 유효할 떄 실행되는 함수**(필수)** - **onInValid** : 해당 Form의 Data들이 유효하지 않을 실행되는 함수**(필수 아님)** ## formState - 해당 **form**의 상태(error, isValid)가 발생했을 때 어떤 값이 이 상태를 발생시켰는지 알려주는 함수들이다. # setError - 직접 Error Trigger를 발생시킬 수 있다. - 보통 isVaild에서 넘긴 값을 통해서 **password**랑 **confirmPassword** 다른지 판별하거나 API를 통해 중복 유저 이름을 판별하여 유효하지 않으면 **Tigger**를 발생시키는 등에 사용된다. - SetError 안에는 어느 **value** 에서 어떠한 **message**를 발동시킬지를 **props**로 넘기면 된다. - `shouldFocus: true` 같은 옵션들도 추가할 수 있다. - Before ```jsx const ToDoList = () => { const [toDo, setToDo] = useState(""); const onChange = (e: React.FormEvent<HTMLInputElement>) => { const {currentTarget: {value}} = e; setToDo(value); }; const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log(toDo); setToDo(""); }; return <div> <form onSubmit={onSubmit}> <input value={toDo} onChange={onChange} type="text" placeholder="Add a todo" /> <button type="submit">Add</button> </form> </div>; }; ``` - After ```jsx import { useForm } from "react-hook-form"; interface FormData { email: string; firstName: string; lastName: string; username: string; password: string; confirmPassword: string; extraError?: string; } const ToDoList = () => { const { register, handleSubmit, formState: { errors }, setError } = useForm<FormData>(); const onValid = (data: FormData) => { if (data.password !== data.confirmPassword) { setError("confirmPassword", { message: "Passwords do not match" }, { shouldFocus: true }); return; } setError("extraError", { message: "Server offline." }); console.log(data); }; const onSubmit = (data: FormData) => { console.log(data); }; return <div> <form style={{ display: "flex", flexDirection: "column", gap: "10px" }} onSubmit={handleSubmit(onValid)}> <input {...register("email", { required: "Email is required", pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$/i, message: "Invalid email address" } })} type="email" placeholder="Email" /> {errors.email && <span>{errors.email.message as string}</span>} <input {...register("firstName", { required: "First Name is required", validate: (value) => { if (value.includes(" ")) { return "First Name cannot contain spaces"; } return true; } })} type="text" placeholder="First Name" /> {errors.firstName && <span>{errors.firstName.message as string}</span>} <input {...register("lastName", { required: "Last Name is required" })} type="text" placeholder="Last Name" /> {errors.lastName && <span>{errors.lastName.message as string}</span>} <input {...register("username", { required: "Username is required", minLength: { value: 10, message: "Username must be at least 10 characters" } })} type="text" placeholder="Username" /> {errors.username && <span>{errors.username.message as string}</span>} <input {...register("password", { required: "Password is required", minLength: { value: 5, message: "Password must be at least 5 characters" } })} type="password" placeholder="Password" /> {errors.password && <span>{errors.password.message as string}</span>} <input {...register("confirmPassword", { required: "Confirm Password is required", minLength: { value: 5, message: "Confirm Password must be at least 5 characters" } })} type="password" placeholder="Confirm Password" /> {errors.confirmPassword && <span>{errors.confirmPassword.message as string}</span>} <button type="submit">Add</button> {errors.extraError && <span>{errors.extraError.message as string}</span>} </form> </div>; }; ``` 💡 하지만 이렇게 Input 내에 validation을 적게 되면 전체적인 코드가 길어져 가독성이 많이 떨어진다. 이를 해결하기 위해 zod 라이브러리를 따로 이용하는게 좋다. ```jsx function ToDo({ text, category }: ToDoData) { const onClick = (newCategory: ToDoData["category"]) => { console.log("i wanna to " + newCategory); } return <li> {text} {category !== "Doing" && ( <button onClick={() => onClick("Doing")}> Doing </button> )} {category !== "Todo" && ( <button onClick={() => onClick("Todo")}> Todo </button> )} {category !== "Done" && ( <button onClick={() => onClick("Done")}> Done </button> )} </li>; } ``` ```jsx import { useSetAtom } from "jotai"; import { ToDoData, toDoState } from "../../../atoms"; function ToDo({ text, category, id }: ToDoData) { const setToDoList = useSetAtom(toDoState); const onClick = (e: React.MouseEvent<HTMLButtonElement>) => { const { currentTarget: { name } } = e; console.log("i wanna to " + name); setToDoList((prev) => prev.map((toDo) => toDo.id === id ? { ...toDo, category: name as ToDoData["category"] } : toDo)); } return <li> {text} {category !== "Doing" && ( <button name="Doing" onClick={onClick}> Doing </button> )} {category !== "Todo" && ( <button name="Todo" onClick={onClick}> Todo </button> )} {category !== "Done" && ( <button name="Done" onClick={onClick}> Done </button> )} </li>; } ``` # **Immutability** - 리스트 안의 요소 바꾸기 ``` const onClick = (e: React.MouseEvent<HTMLButtonElement>) => { const { currentTarget: { name } } = e; setToDoList((prev) => { const targetIndex = prev.findIndex((toDo) => toDo.id === id); const newToDo = {text, id, category: name as ToDoData["category"]}; return [...prev.slice(0, targetIndex), newToDo, ...prev.slice(targetIndex + 1)]; }); } ``` # Enum - 여러 곳에서 같은 **Type**을 명시해야 할 때, 단순히 값으로 이를 관리하면 실수를 하거나, 수정 사항이 있으면 이를 다 바꿔야 되는 상황이 벌어진다. - 이를 방지하기 위해 다양한 곳에서 명시해야하는 **Type**의 경우는 **Enum**으로 관리하고 그때그때 해당 **Enum**을 불러와 지정해주는게 좋다. ```jsx // Enum으로 Category 지정 export enum ToDoCategory { Todo = "Todo", Doing = "Doing", Done = "Done", } // 다양한 곳에서 ToDoCategory 활용 ----------------------------------------------------------------------- export interface ToDoData { id: string; text: string; category: ToDoCategory; } ----------------------------------------------------------------------- export const toDoCategoryState = atom<ToDoCategory>(ToDoCategory.Todo); ----------------------------------------------------------------------- <select value={category} onChange={onInput}> <option value={ToDoCategory.Todo}>To Do</option> <option value={ToDoCategory.Doing}>Doing</option> <option value={ToDoCategory.Done}>Done</option> </select> ----------------------------------------------------------------------- const onInput = (e: React.ChangeEvent<HTMLSelectElement>) => { setCategory(e.target.value as ToDoCategory); } ``` ## 상태 관리 라이브러리(어떤 걸 사용하는 것이 좋을까?) ## Redux(redux-toolkit) - 상태관리 라이브러리 중 가장 오래되었고 그만큼 참고자료가 많다. - **Redux** 자체의 개념을 이해하는데 다른 라이브러리보다 **러닝 커브**가 높다. - **Flux 패턴**을 사용한다. - **redux-toolkit**에 **RTK-query**(서버 상태 관리 라이브러리)도 내장되어 있어 별도의 설치 없이 서버 데이터 캐싱이 redux와 호환이 잘 된다. - 다만, 서버 데이터에 대한 상태 관리가 필요 없을 때는 패키지 용량만 잡아먹는다. 💡 **Flux**는 사용자 입력을 기반으로 Action을 만들고 Action을 Dispatcher에 전달하여 Store(Model)의 데이터를 변경한 뒤 View에 반영하는 단방향의 흐름으로 애플리케이션을 만드는 아키택처를 말한다. ## Recoil - **Meta**에서 발표한 상태 관리 라이브러리이다. - **React Hooks**와 유사하게 동작하여 **러닝 커브**가 낮다. - **atom** 개념을 사용하여 작은 상태를 조합하여 큰 상태를 만든다. - **bottom-up** 방식을 사용한다. - **redux-dev-tools** 같은 디버깅 툴이 완벽하게 지원하지 않고 현재 지원이 중단됐다. ## Zustand - **간소화된 Flux 패턴**을 사용하며 작고 빠르게 확장 가능한 상태 관리 라이브러리이다. - **Reack Hook**을 사용하여 상태를 사용하기 때문에 **추가적인 Hook**이 필요 없다. - **Provider**로 감싸지 않아도 되기 때문에 구조가 더 단순하다. - **상태가 변경될 때만** 컴포넌트를 랜더링 하기 때문에 불필요한 렌더링을 감소시킬 수 있다. ## Jotai - **Recoil**의 영감을 받아서 등장한 라이브러리이며 Recoil과 동일하게 **atomic** 패턴을 따른다. - **React Hooks**과 매우 유사하게 동작한다. - **Provider**로 감싸지 않아도 되기 때문에 구조가 더 단순하다. - SSR, 비동기, Family 등 다양한 유틸리티가 지원된다. - 참고 자료가 방대하지 않고 공식적인 **DevTools**이 지원되지 않는다. ## Redux(redux-toolkit) VS Zustand VS Jotai - 2025-05-19일 기준 ![[Redux Zustand Jotai 비교 1.png]] ![[Redux Zustand Jotai 비교 2.png]] 출처 - [https://npmtrends.com/jotai-vs-redux-vs-zustand](https://npmtrends.com/jotai-vs-redux-vs-zustand) ## 개인적인 선택 - 대규모 프로젝트에서 많은 기능을 요구하며 참고 자료가 많아야 한다면 **redux** - 가볍고 간편하게 이용하길 원한다면 **jotai**