티스토리 뷰
React 상태 관리
▶ 상태 관리에 사용되는 Hooks
useState, useRef, useContext, useReducer
외부 라이브러리 없이 React가 제공하는 Hook만으로 상태 관리를 구현하기 위해 사용한다.
함수형 컴포넌트에 상태를 두고 여러 컴포넌트 간 데이터와 데이터 변경 함수를 공유하는 방식으로 상태를 관리하게 된다.
1) useState
const [ state, setState ] = useState(initState | initFn)
- 단순한 하나의 상태를 관리하기에 적합하다.
- state가 바뀌면 state를 사용하는 컴포넌트를 리렌더링한다.
- useEffect와 함께 state에 반응하는 훅을 구축할 수 있다.
2) useRef
상태가 바뀌어도 리렌더링하지 않는 상태를 정의한다.
즉, 상태가 UI의 변경과 관계없을 때 사용한다. ex) setTimeout의 timerId 저장
uncontrolled component의 상태를 조작하는 등 리렌더링을 최소화하는 상태 관리에 사용된다.
ex) Dynamic Form 예시
3) useContext
Context Provider 안에서 렌더링되는 컴포넌트는 useContext를 이용해
깊이 nested 된 컴포넌트라도 바로 context value를 가져온다.
- 컴포넌트와 컴포넌트 간 상태를 공유할 때 사용된다.
- 부분적인 컴포넌트들의 상태 관리, 전체 앱의 상태 관리를 모두 구현할 수 있다.
- context value가 바뀌면 내부 컴포넌트는 모두 리렌더링된다.
4) useReducer
const [state, dispatch] = useReducer(reducer, initState)
- useState보다 복잡한 상태를 다룰 때 사용한다.
- 별도의 라이브러리 없이 flux pattern에 기반한 상태 관리를 구현할 수 있다.
- nested state 등 복잡한 여러 개의 상태를 한꺼번에 관리하거나 어떤 상태에 여러 가지 처리를 적용할 때 유용하다.
- 상태 복잡하다면 useState에 관한 callback을 내려주는 것보다 dispatch를 prop으로 내려 리렌더링을 최적화하는 것을 권장한다.
▶ useState를 활용한 상태 관리
상위 컴포넌트에서 state와 state 변경 함수를 정의하고
그 state나 변경 함수를 사용하는 컴포넌트까지 prop으로 내려주는 패턴
- state가 변경되면 중간에 state를 넘기기만 하는 컴포넌트들도 모두 리렌더링된다.
- 상태와 상태에 대한 변화가 단순하거나 상대적으로 소규모 앱에서 사용하기에 적합하다.
// TodoApp.jsx
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
const [globalId, setGlobalId] = useState(3000);
const toggleTodo = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const addTodo = (title) => {
setTodos((todos) => [{ title, id: globalId + 1}, ...todos]);
setGlobalId((id) => id + 1);
};
return <TodosPage
state={{ todos, filter }}
toggleTodo={toggleTodo}
addTodo={addTodo}
deleteTodo={deleteTodo}
changeFilter={setFilter}
/>
}
// TodosPage.jsx
function TodosPage({ state, addTodo, deleteTodo, toggleTodo, changeFilter }) {
const filteredTodos = state.todos.filter((todo) => {
const { filter } = state;
return (
filter === "all" ||
(filter === "completed" && todo.completed) ||
(filter === "todo" && !todo.completed)
);
});
return (
<div>
<h3>TodosPage</h3>
<TodoForm onSubmit={addTodo} />
<TodoFilter filter={state.filter}
changeFilter={changeFilter} />
<TodoList
todos={filteredTodos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
</div>
);
}
// TodoForm.jsx
function TodoForm({ onSubmit }) {
const [title, setTitle] = useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit(title);
setTitle("");
}}
>
<label htmlFor="todo-title">Title</label>
<input id="todo-title" type="text"
name="todo-title"
onChange={(e) => setTitle(e.target.value)}
value={title} />
<button type="submit">Make</button>
</form>
);
}
function TodoList({ todos, toggleTodo, deleteTodo }) {
return (
<ul>
{todos.map(({ title, completed, id }) => (
<li onClick={() => toggleTodo(id)}>
<h5>{title}</h5>
<div>
{completed ? "☑️ " : "✏️ "}
<button onClick={() => deleteTodo(id)}>Delete</button>
</div>
</li>
))}
</ul>
);
}
// TodoFilter.jsx
function TodoFilter({ filter, changeFilter }) {
return (
<div>
<label htmlFor="filter">Filter</label>
<select
onChange={(e) => changeFilter(e.target.value)}
id="filter"
name="filter"
>
{filterList.map((filterText) => (
<option selected={filter === filterText} value={filterText}>
{capitalize(filterText)}
</option>
))}
</select>
</div>
);
}
▶ useContext를 활용한 상태 관리
Provider 단에서 상태를 정의하고 직접 상태와 변경 함수를 사용하는 컴포넌트에서
useContext를 이용해 바로 상태를 가져와 사용하는 패턴
- useReducer와 함께 복잡한 상태와 상태에 대한 변경 로직을 두 개 이상의 컴포넌트에서 활용하도록 구현이 가능하다.
- state는 필요한 곳에서만 사용하므로 불필요한 컴포넌트 리렌더링을 방지할 수 있다.
- Prop Drilling(Plumbing)을 방지하여 컴포넌트 간 결합도를 낮춘다.
// TodoContext.jsx
const TodoContext = createContext(null);
const initialState = {
todos: [],
filter: "all",
globalId: 3000,
};
function useTodoContext() {
const context = useContext(TodoContext);
if (!context) {
throw new Error("Use TodoContext inside Provider.");
}
return context;
}
function TodoContextProvider({ children }) {
const values = useTodoState(); // 아래 useTodoState함수가 return하는 object
return <TodoContext.Provider value={values}>{children}</TodoContext.Provider>;
}
function reducer(state, action) {
switch (action.type) {
case "change.filter":
return { ...state, filter: action.payload.filter };
case "init.todos":
return { ...state, todos: action.payload.todos };
case "add.todo":
return { ...state,todos: [{ title: action.payload.title, id: state.globalId + 1 }], globalId: state.globalId + 1 };
case "delete.todo":
return { ...state, todos: state.todos.filter((todo) => todo.id !== action.payload.id) };
case "toggle.todo":
return { ...state,todos: state.todos.map((t) => t.id===action.payload.id ? { ...t, completed: !t.completed } : t)};
default:
return state;
}
}
function useTodoState() {
const [state, dispatch] = useReducer(reducer, initialState);
const toggleTodo = useCallback( (id) => dispatch({ type: "toggle.todo", payload: { id } }), []);
const deleteTodo = useCallback( (id) => dispatch({ type: "delete.todo", payload: { id } }), []);
const addTodo = useCallback( (title) => dispatch({ type: "add.todo", payload: { title } }), []);
const changeFilter = useCallback( (filter) => dispatch({ type: "change.filter", payload: { filter } }), []);
const initializeTodos = useCallback( (todos) => dispatch({ type: "init.todos", payload: { todos } }), []);
return { state, toggleTodo, deleteTodo, addTodo, changeFilter, initializeTodos };
}
// TodoApp.jsx
function TodoApp() {
return (
<TodoContextProvider>
<TodosPage />
</TodoContextProvider>
);
}
//TodosPage.jsx
function TodosPage() {
const { initializeTodos } = useTodoContext();
useEffect(() => {
console.log("useEffect");
fetchTodos().then(initializeTodos);
}, [initializeTodos]);
return (
<div>
<TodoForm />
<TodoFilter />
<TodoList />
</div>
);
}
// TodoForm.jsx
function TodoForm() {
const { addTodo } = useTodoContext();
const [title, setTitle] = useState("");
return (
<form onSubmit={(e) => {
e.preventDefault();
addTodo(title);
setTitle("");
}}>
<label htmlFor="todo-title">Title</label>
<input
id="todo-title"
type="text"
name="todo-title"
onChange={(e) => setTitle(e.target.value)}
value={title} />
<button type="submit">Make</button>
</form>
);
}
function TodoList() {
const { state, toggleTodo, deleteTodo } = useTodoContext();
const { todos, filter } = state;
const filteredTodos = todos.filter((todo) => {
return (
filter === "all" ||
(filter === "completed" && todo.completed) ||
(filter === "todo" && !todo.completed)
);
});
return (
<ul>
{filteredTodos.map(({ title, completed, id }) => (
<li key={id} onClick={() => toggleTodo(id)}>
<h5>{title}</h5>
<div>
{completed ? "☑️ " : "✏️ "}
<button onClick={() => deleteTodo(id)}>Delete</button>
</div>
</li>
))}
</ul>
);
}
// TodoFilter.jsx
function TodoFilter() {
const { state, changeFilter } = useTodoContext();
const { filter } = state;
return (
<div>
<label htmlFor="filter">Filter</label>
<select
onChange={(e) =>
changeFilter(e.target.value)}
id="filter"
name="filter"
value={filter}
>
{filterList.map((filterText) => (
<option key={filterText} value={filterText}>{capitalize(filterText)}</option>
))}
</select>
</div>
);
}
이 글은 엘리스의 AI트랙 5기 강의를 들으며 정리한 내용입니다.
'개발공부 > 🟦 React.js' 카테고리의 다른 글
[React] React 테스팅 (0) | 2023.02.15 |
---|---|
[React] class와 hooks 비교하기 (0) | 2023.02.14 |
[React] Flux Pattern (0) | 2022.08.19 |
[React] 상태 관리 (0) | 2022.08.19 |
[React] POSTMAN, OpenAPI, CORS (0) | 2022.08.16 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그