티스토리 뷰

개발공부/🟦 React.js

[React] 상태 관리에 사용되는 Hooks

2022. 8. 19. 23:55
 

React – A JavaScript library for building user interfaces

A JavaScript library for building user interfaces

reactjs.org

 

 

 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
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그