# 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**