1. React 상태관리란?
리엑트는 우선 컴포넌트 기반의 라이브러리다. 이 컴포넌트는 각각이 독립적이다. props를 통해 부모-자식 간에 상태를 전파할 수있다.
하지만 컴포넌트가 많아지고 관리해야하는 상태 값이 많아진다면 상태가 시작된 시점과 어떤 컴포넌트들을 거쳐가는지 모든 흐름을 이해하기 힘들어진다. 상태를 어떻게 관리하는지에 따라 렌더링에 영향을 미칠 수 있다. 프론트엔드 개발자에게 상태관리는 중요한 임무가 되었다.
그렇기 때문에 상태 관리 툴을 이용하여 효율적으로 상태를 관리할 필요가 있다.
상태 관리를 위한 툴로는 Context API, Redux, React Query, Zustand 등이 널리 사용되고 있다.
2. Zustand란?
Zustand란 상태라는 뜻을 가진 독일어이다.
Zustand를 사용하는 이유는 다음과 같다.
1. 사용 방법이 매우 간단하다.
바닐라 자바스크립트를 기준으로 핵심 로직의 코드 줄 수가 약 42줄밖에 되지 않는다.
2. 상태가 변경되면 불필요한 리렌더링을 일으키지 않는다.
3. 보일러플레잍가 거의 없다.
보일러플레이트란 최소한의 변경으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 양상을 말한다.
4. redux Devtools를 사용할 수 있어 디버깅에 용이하다.
Zustand를 사용하는 이유는, 원래는 redux를 알고 있었지만 다른 프론트 분께서 redux 보다 더 간단한 상태관리 툴인 zustand를 추천해주셨기 때문이다.
3. 사용 방법
1. Zustand 설치
npm i zustand #or yarn add zustand
2. store(상태 관리 컴포넌트) 생성, store 선언
함수 : (인자) => set(state => ( {상태 } )
set함수에서 인자로 넘겨지는 state는 현재 상태를 의미 함수를 선언한 이후 state를 업데이트한다. 이 모든 상태 업데이트 함수들은 useStore에 저장하고 create함수로 zustand를 불러온다.
//store.js
import create from 'zustand'
//set method로 상태 변경 가능
const useStore = create(set => ({ //create로 zustand를 불러온다.
count: 0,
increaseCount: () => set(state => ({count: state.count +1 })),
setThree: (input) => set({ count: input}),
}));
export default useStore
3. Store 사용
컴포넌트에서 state를 불러올 때는 useStore로 동적으로 함수를 불러와도 되고,
const count = useStore(state => state.count) 이런식으로 불러와도 된다.
어쨌든 동적 객체의 상태가 불려진다.
//App.js
function App() {
const { count, increaseCount, setThree } = useStore; // useStore 객체 동적으로 불러오기
return (
<div className = 'App'>
<div>Zustand ! {count} </div>
<button onClick = {increaseCount}>+1</button>
<button onClick = {() => setNum(3)>set3</button>
</div>
);
}
4. devtools를 사용하여 디버깅하기
Redux devtools를 크롬 웹 스토어에서 설치 해준 후, 아래처럼 store와 devtools를 연결해준다. 어플리케이션을 띄우고 개발자도구 창에서 Redux devtools를 확인하면 store의 상태를 확인할 수 있다.
//store.js
import create from 'zustand'
import { devtools} from 'zustand/middleware'
//set method로 상태 변경 가능
const useStore = create(set => ({ //create로 zustand를 불러온다.
count: 0,
increaseCount: () => set(state => ({count: state.count +1 })),
setThree: (input) => set({ count: input}),
}));
const useStore = create(devtools(store))
export default useStore
Todo list로 상태 관리하기 zustand
공식문서
export interface Todo {
id: string;
description: string;
completed: boolean;
}
//typescript 정의
zustand store 생성하기
set : zustand의 상태는 set을 통해 관리 된다.
import create from 'zustand'
import { v4 as uuidv4} from "uuid";
import {Todo} from "./model/Todo";
interface TodoState {
todos: Todo[];
addTodo: (description: string) => void;
removeTodo : (id: string)=> void;
toggleCompletedState: (id: string) => void;
}
export const useStore = create<TodoState> ((set) => ( { //set을 통해 관리하는 zustand
todos: [], //todo 배열 선언
add : (description: string) => { //description을 인자. add 함수 상태 함수 선언, 함수로 set
set((state) => ({
todos: [
...state.todos, //현재 todos배열의 상태
{
id: uuidv4(),
description,
completed: false,
}as Todo, //todo객체 추가
],
}));
},
removeTodo: (id) => { //id 인자로 받음
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
}));
},
}
zustand react에서 사용하기
import { useState } from "react";
import {
Button,
Checkbox,
Container,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
makeStyles,
TextField,
Typography,
} from "@material-ui/core"; //icon 불러옴
import DeleteIcon from "@material-ui/icons/Delete";
import { useStore } from "./todoStore"; //********todostore에서 useStore import해오기
const useStyles = makeStyles((theme) => ({
headerTextStyles: {
textAlign: "center",
marginBottom: theme.spacing(3),
},
textBoxStyles: {
marginBottom: theme.spacing(1),
},
addButtonStyles: {
marginBottom: theme.spacing(2),
},
completedTodoStyles: {
textDecoration: "line-through",
},
}));
function App() {
const {
headerTextStyles,
textBoxStyles,
addButtonStyles,
completedTodoStyles,
} = useStyles();
const [todoText, setTodoText] = useState("");
const { addTodo, removeTodo, toggleCompletedState, todos } = useStore(); //*****useStore 동적 객체 불러오기****
//addTodo state함수, removeTodo state함수, todo배열도 불러오기
//addTodo(TodoText) TodoText는 description으로 인자로 받는다.
return (
<Container maxWidth="xs">
<Typography variant="h3" className={headerTextStyles}>
To-Do's
</Typography>
<TextField
className={textBoxStyles}
label="Todo Description"
required
variant="outlined"
fullWidth
onChange={(e) => setTodoText(e.target.value)}
value={todoText}
/>
<Button
className={addButtonStyles}
fullWidth
variant="outlined"
color="primary"
onClick={() => {
if (todoText.length) {
addTodo(todoText);
setTodoText("");
}
}}
>
Add Item
</Button>
<List>
{todos.map((todo) => (
<ListItem key={todo.id}>
<ListItemIcon>
<Checkbox
edge="start"
checked={todo.completed}
onChange={() => toggleCompletedState(todo.id)}
/>
</ListItemIcon>
<ListItemText
className={todo.completed ? completedTodoStyles : ""}
key={todo.id}
>
{todo.description}
</ListItemText>
<ListItemSecondaryAction>
<IconButton
onClick={() => {
removeTodo(todo.id);
}}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Container>
);
}
export default App;
const { addTodo, removeTodo, toggleCompletedState, todos } = useStore();
동적 객체로 useStore의 함수를 불러온다.
적용하기 (다크모드 state zustand로 관리하기)
store.js
import create from 'zustand'
const useDarkModeStore = create(set => ({
darkMode: false,
toggleDarkMode: () =>
set(state => ({darkMode : !state.darkMode})
)
}));
export default useDarkModeStore;
component 적용
import React, { useState } from 'react';
import styled from 'styled-components';
import CloseIcon from '@mui/icons-material/Close';
import { useDarkModeStore }from '../store'
function LoginModal() {
const { darkMode } = useDarkModeStore();
const [isJoin, setIsJoin] = useState(false);
function isJoinHandler() {
setIsJoin(!isJoin);
}
let Mode = '로그인';
if(isJoin === true){
Mode = '로그인';
}
else{
Mode = '회원가입';
}
return (
<LoginModalLayout >
<LoginBox>
<WelcomBox darkMode = {darkMode} >환영합니다!</WelcomBox>
<InitialBox darkMode = {darkMode}>
<Close>
<CloseIcon />
</Close>
<Contents>
<Title darkMode = {darkMode}>{Mode}</Title>
<Input darkMode = {darkMode}>
<div>이메일로 {Mode}</div>
<input placeholder="이메일을 입력하시오." />
<button>{Mode}</button>
</Input>
<Social darkMode = {darkMode}>소셜 계정으로 {Mode}</Social>
<ButtonBox>
<button>
<GitHubIcon src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRh4BoE0qpxWdx6TWAu8BgtWfSQKGn6hqPTOw&usqp=CAU" />
</button>
<button>
<GoogleIcon src="https://cdn.icon-icons.com/icons2/836/PNG/512/Google_icon-icons.com_66793.png" />
</button>
<button>
<FaceBookIcon src="https://www.chsica.org/wp-content/uploads/2020/10/Facebook-Logo-PNG-Transparent-Like-17-300x300.png" />
</button>
</ButtonBox>
<ChangeMode darkMode = {darkMode}>
{!isJoin && <p>아직회원이 아니신가요?</p>}
{isJoin && <p>이미 계정이 있으신가요?</p>}
<button onClick ={isJoinHandler}>{Mode}</button>
</ChangeMode>
</Contents>
</InitialBox>
</LoginBox>
</LoginModalLayout>
);
}
export default LoginModal;
styled component에 적용 예시
const WelcomBox = styled.div`
width: 216px;
font-family : 'Rubik', 'sans-serif';
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 600;
text-align: center;
font-family: 'Rubik', sans-serif;
color: ${(props) =>
props.darkMode ? '#d9d9d9' : '#495057'};
background-color: ${(props) =>
props.darkMode ? '#1e1e1e' : '#f8f9fa'};
@media ( max-width: 770px ) {
display: none;
}
`;