ABOUT ME

-

오늘
-
어제
-
-
  • [React] Context로 언제 어디서든 상태 관리 해보기
    Front-end/React 2021. 1. 1. 21:35

    상태 관리

    React의 영원한 과제 상태 관리.. 이 상태관리를 위해 정말 많은 라이브러리가 탄생했습니다.

    대표적으로 가장 많이 사용하는 Redux도 있고, Mobx, Recoil 등이 있습니다.

     

    컴포넌트는 생성 될 때부터 끝날 때까지 가지고 있는 상태를 책임지게 되는데요, 즉 이말은 컴포넌트 내에서 생성된 상태는 외부에서 침범할 수 없다는 것을 의미합니다.

    그렇다면 어떻게 해야 이 상태를 외부에서도 사용 및 관리를 할 수 있을까요?

     

    첫 번째로는 상위 컴포넌트에서 상태를 관리하여 props로 내려주는 방법이 있습니다. 상위 컴포넌트에서 상태를 만들고 필요한 컴포넌트까지 prop으로 넘겨주는 것은 마치 축구 경기시 공격수가 공을 필요로 할 때 골키퍼 -> 수비수 -> 미드필더 -> 공격수까지 순서를 거치며 공을 배급하는 것과 같습니다.

    이런 방식으로 진행되면 간단한 프로젝트에서는 큰 고민없이 해결이 되겠지만, 프로젝트 규모가 커질수록 컴포넌트가 많아지고, 깊이가 깊어지면 그만큼 많이 넘겨주게 되어 굉장히 복잡하고 쓸데없는 데이터 이동이 일어나게 되겠죠

     

    두 번째로는 전역에서 관리를 하며 필요할 때마다 가져다 사용하는 방법이 있습니다. 지금 알아볼 Context라는 개념입니다. 컨텍스트를 사용하게 되면 상위 컴포넌트에서 관리하면서 내려줄 필요 없이 그냥 가져다가 사용하면 됩니다. 다시 축구로 예를 들자면 골키퍼가 롱패스로 바로 공격수에게 공을 배급하는 것입니다. 훨씬 간결하겠네요!

     

     

    간단하게 Todo를 통하여 생성과 읽기 기능을 만들며 어떻게 전역에서 상태를 관리하는지 살펴보겠습니다.

    기본적으로 프로젝트 세팅이 되어있다는 가정 하에 진행하겠습니다.

     

    ❗️ 이 글은 개인적으로 학습한 정보와 지식을 토대로 작성된 글입니다. 혹시 잘못된 부분이 있거나 수정사항이 있다면 말씀해주시면 반영하겠습니다. 🙏

    context/index.tsx

    컨텍스트를 구현하기 위해서는 createContext, useContext Hooks를 통해 만들게 됩니다.

    위 두개의 키워드에 집중하여 코드를 보겠습니다.

    import React, { useState, useEffect, createContext, useContext } from 'react';
    
    // todo 타입 정의
    export interface Todo {
      id: number;
      text: string;
      isDone: boolean;
    }
    
    // 전역에서 사용할 컨텍스트를 생성
    const TodoStateContext = createContext<Todo[] | undefined>(undefined);
    const TodoSetStateContext = createContext<
      React.Dispatch<React.SetStateAction<Todo[]>> | undefined
    >(undefined);
    
    // 컨텍스트를 적용하고 사용할 컴포넌트를 지정
    const TodoProvider: React.FC = ({ children }) => {
      const [todos, setTodos] = useState<Todo[]>([]);
    
      useEffect(() => {
        setTodos(JSON.parse(`${window.localStorage.getItem('todos')}`) || []);
      }, []);
    
      return (
        <TodoStateContext.Provider value={todos}>
          <TodoSetStateContext.Provider value={setTodos}>
            {children}
          </TodoSetStateContext.Provider>
        </TodoStateContext.Provider>
      );
    };
    
    export default TodoProvider;
    
    // 컨텍스트를 보다 편하게 사용할 커스텀 훅 구현
    export function useTodoStateContext() {
      const context = useContext(TodoStateContext);
      if (!context) {
        throw new Error('context is not found');
      }
      return context;
    }
    
    export function useTodoSetStateContext() {
      const context = useContext(TodoSetStateContext);
      if (!context) {
        throw new Error('context is not found');
      }
      return context;
    }
    

    가장 먼저 createContext를 통하여 할 일들의 상태와 할 일을 set 해주는 컨텍스트를 만들었습니다. 이렇게 만들어주면 해당 컨텍스트를 생성한 단계까지 온 것입니다.

     

    그리고 컨텍스트를 적용하기 위해 Provider 컴포넌트를 만들게 되는데요 여기서는 전역으로 사용될 상태를 만들고, 이전에 생성한 컨텍스트에 .Provider를 통해 value에 상태를 담아주고, children을 선언하여 Provider 내부의 컴포넌트를 대상으로 생성된 컨텍스트를 언제든 사용할 수 있도록 만들어줍니다. 즉, children에 해당되는 컴포넌트는 컨텍스트를 사용할 수 있는 단계가 된 것입니다.

    현재 코드에서는 todos와 setTodos를 모두 전역에서 사용하기 위해 두 Provider를 만들어 value를 선언해 내부에 children을 선언한 모습입니다.

     

    원래는 여기까지 사용하면 되지만 외부 컴포넌트에서 보다 편리하게 컨텍스트를 사용할 수 있도록 커스텀 훅을 만들어주었습니다.

    현재 createContext를 통해 두 개의 컨텍스트를 생성했죠?

    이렇게 되면 컨텍스트를 사용할 때 두 개 중 필요한 컨텍스트를 선택하여 사용할 수 있게 됩니다.

    이걸 사용하는 Hooks가 바로 useContext입니다.

    useContext에 내가 사용한 컨텍스트를 가져오게 되면 value에 선언한 값을 가져와서 사용할 수 있게되는 것입니다.

    App.tsx

    import React from 'react';
    import './App.css';
    import TodoProvider from './context';
    import Header from './components/header';
    import Input from './components/input';
    import List from './components/list';
    
    function App() {
      return (
        <div className='App'>
          <TodoProvider>
            <Header />
            <Input />
            <List />
          </TodoProvider>
        </div>
      );
    }
    
    export default App;
    

    컨텍스트를 만들 때 children을 대상으로 컨텍스트를 사용 할 수 있다고 했었죠?

    그래서 원하는 컴포넌트의 부모 컴포넌트로 선언해주면 자식 컴포넌트들은 컨텍스트를 사용할 수 있게 됩니다.

     

    이제 본격적으로 만들어진 컨텍스트를 사용해보겠습니다.

     

    components/header.tsx

    import React from 'react';
    import styled from 'styled-components';
    
    const Text = styled.header`
      margin: 0 0 10px 0;
      font-size: 22px;
    `;
    
    const Header: React.FC = () => {
      return <Text>Context TodoList</Text>;
    };
    
    export default Header;
    

    특별한 로직이 없는 헤더 컴포넌트입니다. 

    components/input.tsx

    input에서는 할 일을 입력한 값에 대해 새롭게 추가하는 역할을 담당합니다.

    그렇다면 우리가 만든 todos와 setTodos의 컨텍스트 모두 필요하다는 것을 인지하고 코드를 보겠습니다.

    import React from 'react';
    import styled from 'styled-components';
    import { Todo, useTodoSetStateContext, useTodoStateContext } from '../context';
    
    const Form = styled.form`
      margin: 0 0 20px 0;
    `;
    
    const Input: React.FC = () => {
      const [text, setText] = React.useState<string>('');
    
      // 커스텀 훅을 통해 가져온 컨텍스트
      const setTodos = useTodoSetStateContext();
      const todos = useTodoStateContext();
    
      const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const newTodo: Todo = {
          id: new Date().getTime(),
          text,
          isDone: false,
        };
    
        const newTodos = [...todos, newTodo];
        window.localStorage.setItem('todos', JSON.stringify(newTodos));
        
        // 가져온 컨텍스트를 사용
        setTodos(newTodos);
        setText('');
      };
    
      const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setText(event.target.value);
      };
    
      return (
        <Form onSubmit={handleSubmit}>
          <input
            value={text}
            placeholder='Todo...'
            onChange={handleChange}
            autoComplete='off'
            required
          />
        </Form>
      );
    };
    
    export default Input;
    

    컨텍스트에서 만들었던 커스텀 훅을 사용하여 상태를 가져왔습니다.

    이를 통해 input이 최종적으로 submit이 될 때 setTodos를 사용하여 할 일을 추가하게 되겠죠

     

    만약 아까 서론에서 설명드린 첫 번째 방법으로 했다면 상위 컴포넌트에서 상태를 관리하고 있었기 때문에 submit 부분에서 직접 상태를 조작하지 못하고 prop으로 내려받은 상태로 처리를 했을 것입니다.

    하지만 이제는 그럴 필요 없이 바로 컨텍스트를 가져와서 처리를 하니 훨씬 코드가 간결하게 되었습니다.

     

    그렇다면 이렇게 처리한 상태를 직접 화면에 그려주어야겠죠?

    components/list.tsx

    import React from 'react';
    import { useTodoStateContext } from '../context';
    
    const List: React.FC = () => {
      // 커스텀 훅을 통해 가져온 컨텍스트
      const todos = useTodoStateContext();
    
      return (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>{todo.text}</li>
          ))}
        </ul>
      );
    };
    
    export default List;
    

    input 컴포넌트에서 했던 방식과 차이는 없습니다.

    마찬가지로 커스텀 훅을 사용해서 바로 상태에 접근하여 사용할 수 있습니다.

     

    만약 컨텍스트를 활용하지 않았더라면 할 일에 대한 상태를 상위 컴포넌트에서 받았을테고, 이를 사용했을 것입니다.

     

    그럼 잘 실행되는지 확인해보겠습니다.

    완벽하게 잘 실행됩니다!

     

    이렇게 컨텍스트를 사용해서 전역으로 상태관리를 간단하게나마 해보았는데요, 최근에는 이렇게 간단한 프로젝트 뿐만 아니라 어느정도 규모가 있는 프로젝트에서도 많이 사용하고 있는 추세기도 합니다. 그리고 다른 상태관리 라이브러리를 사용하시게 되더라도 이 컨텍스트의 개념을 가지고 사용하시면 보다 쉽고 편하게 다가올 계기가 될 수 있을 것 같습니다. 😀

    댓글