[React] Context API를 사용하여 전역 값 관리하기

2021. 6. 7. 19:45Front-end/React

728x90
반응형

Context API를 사용하여 전역 값 관리하기

 

App 컴포넌트에서 onRemove, onToggle 함수가 있고 App -> UserList -> User 이렇게 전달이 되는 구조일 때 사실상 UserList에서는 onRemove, onToggle 함수가 직접적으로 사용이 되지 않음에도 불구하고 User 컴포넌트에게 전달해주기 위해서 App에서 props로 받아와서 User 컴포넌트에게 전달해주고 있습니다. 다리역할만 하고 있는 것이죠.

지금 같은 상황은 크게 문제가 되지 않으나 컴포넌트의 구조가 복잡해진다면 이런 구조로 계속 하위 컴포넌트에 전달이 되는 구조로 작성한다면 난해해질 것입니다.

이런 상황은 Context API를 사용하여 해결할 수 있습니다.

 

import React from "react";

const Child = ({ text }) => {
  return <div>안녕하세요? {text}</div>;
};

const Parent = ({ text }) => {
  return <Child text={text} />;
};

const GrandParent = ({ text }) => {
  return <Parent text={text} />;
};

const ContextSample = () => {
  return <GrandParent text="GOOD" />;
};

export default ContextSample;

Context API를 알아보기 위한 간단한 예제인 ContextSample 컴포넌트 입니다.

ContextSample 컴포넌트는 ContextSample => GrandParent => Parent => Child로 text가 계속 전달되어 결국에는 Child 컴포넌트에서 "안녕하세요? GOOD"가 보이는 구조입니다.

우리는 이것을 ContextSample에서 바로 Child로 전달할 수 있도록 바꾸어보겠습니다.

 

createContext로 컨텍스트 만들기

const MyContext = createContext("default value");

createContext를 사용하여 컨텍스트를 만들 수 있습니다. 괄호 안에는 초기 값을 지정할 수 있습니다. 이 초기 값은 값이 지정되지 않는다면 보이는 값입니다.

그리고 이렇게 만든 컨텍스트를 Child 컴포넌트에서 바로 불러와서 사용할 수 있습니다. (중간 다리를 거치지 않고도 바로)

 

useContext로 컨텍스트 사용하기

useContext는 컨텍스트에 있는 값을 읽어와서 사용할 수 있게 해주는 리액트 내장 Hook입니다.

import React, { createContext, useContext } from "react";

const MyContext = createContext("default value");

const Child = () => {
  const text = useContext(MyContext);
  return <div>안녕하세요? {text}</div>;
};

const Parent = ({ text }) => {
  return <Child text={text} />;
};

const GrandParent = ({ text }) => {
  return <Parent text={text} />;
};

const ContextSample = () => {
  return <GrandParent text="GOOD" />;
};

export default ContextSample;

값이 정해지지 않았기 때문에 위와 같이 작성하면 '안녕하세요? default value'라고 뜹니다.

 

컨텍스트 안의 값을 지정하고 싶다면? Provider 사용하기

import React, { createContext, useContext } from "react";

const MyContext = createContext("default value");

const Child = () => {
  const text = useContext(MyContext);
  return <div>안녕하세요? {text}</div>;
};

const Parent = () => {
  return <Child />;
};

const GrandParent = () => {
  return <Parent />;
};

const ContextSample = () => {
  return (
    <MyContext.Provider value="GOOD">
      <GrandParent />
    </MyContext.Provider>
  );
};

export default ContextSample;

컨텍스트의 값을 변경하고 싶다면 컨텍스트를 사용하는 맨 위에 있는 곳에서 MyContext.Provider처럼 Provider 컴포넌트를 이용하여 감싸주면 되고 value로 값을 전달하면 값이 변경됩니다.

 

🌞 지금 구조를 살펴보면 MyContext.Provider를 통해서 value값을 설정해주었으므로 MyContext의 값이 설정이 되는거고 그리고 Child 컴포넌트에서 useContext를 이용해서 MyContext의 값을 그대로 불러와서 값을 사용할 수가 있었던 것입니다.

🌞 그리고 MyContext같은 이런 컨텍스트를 다른 파일에서 작성하고 내보낸다음 불러와서 어디서든지 사용할 수 있습니다.

🌞 그리고 당연히 컨텍스트의 값은 유동적으로 변경 가능합니다.


User 컴포넌트에 적용하기

App 컴포넌트에서 만들어진 onRemove, onToggle 함수를 UserList를 거치지 않고 바로 User 컴포넌트에서 사용하도록 해보겠습니다.

const [state, dispatch] = useReducer(reducer, initialState);

const onRemove = useCallback((id) => {
    dispatch({
      type: "REMOVE_USER",
      id,
    });
  }, []);

  const onToggle = useCallback((id) => {
    dispatch({
      type: "TOGGLE_USER",
      id,
    });
  }, []);

현재 onRemove와 onToggle 함수는 이렇게 되어있으므로 UserDispatch의 값으로는 dispatch를 넣어주면 됩니다

 

import React, {
  useReducer,
  useRef,
  useMemo,
  useCallback,
  createContext,
} from "react";
import "./App.css";
import CreateUser from "./CreateUser";
import useInputs from "./useInputs";
import UserList from "./UserList";

function countActiveUsers(users) {
  return users.filter((user) => user.active).length;
}
const initialState = {
  users: [
    {
      id: 1,
      username: "velopert",
      email: "public.velopert@gmail.com",
      active: false,
    },
    {
      id: 2,
      username: "tester",
      email: "tester@example.com",
      active: false,
    },
    {
      id: 3,
      username: "liz",
      email: "liz@example.com",
      active: false,
    },
  ],
};
const reducer = (state, action) => {
  switch (action.type) {
    case "CREATE_USER":
      return {
        users: state.users.concat(action.user),
      };
    case "REMOVE_USER":
      return {
        ...state,
        users: state.users.filter((user) => user.id !== action.id),
      };
    case "TOGGLE_USER":
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.id ? { ...user, active: !user.active } : user
        ),
      };
    default:
      throw new Error("Unhandled action");
  }
};

export const UserDispatch = createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [form, onChange, reset] = useInputs({
    username: "",
    email: "",
  });
  const { username, email } = form;
  const nextId = useRef(4);
  const { users } = state;

  const onCreate = useCallback(() => {
    dispatch({
      type: "CREATE_USER",
      user: {
        id: nextId.current,
        username,
        email,
      },
    });
    reset();
    nextId.current += 1;
  }, [username, email, reset]);

  const onRemove = useCallback((id) => {
    dispatch({
      type: "REMOVE_USER",
      id,
    });
  }, []);

  const onToggle = useCallback((id) => {
    dispatch({
      type: "TOGGLE_USER",
      id,
    });
  }, []);

  const count = useMemo(() => countActiveUsers(users), [users]);

  return (
    <UserDispatch.Provider value={dispatch}>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성 사용자 수 : {count} </div>
    </UserDispatch.Provider>
  );
}

export default App;

지금 상황을 보면 App 컴포넌트에서 UserDispatch라는 컨텍스트를 만든 것입니다.

그리고 UserDispatch 컨텍스트의 값은 useReducer를 통해 받아온 dispatch라는 것을 value로 넣어준 상태입니다.

그러면 바로 User 컴포넌트에서 onRemove, onToggle을 어떻게 사용할 수 있을까요?

 

User 컴포넌트에서 컨텍스트 사용해보기

  • App 컴포넌트에서 onToggle, onRemove 함수가 필요없으므로 지워준다.
  • UserList에 전달했던 onToggle, onRemove 지워준다.
  • UserList 파일에서 props로 받아왔던 onToggle, onRemove 지워주고 User에 전달하는 것도 지워준다.
  • 마찬가지로 User 컴포넌트에서도 props로 받아온 onToggle, onRemove 지워준다.

우리는 User 컴포넌트에서 onToggle, onRemove를 정의할 것입니다. 왜냐하면 UserDispatch라는 컨텍스트로 dispatch를 받아오고 있기 때문입니다.

import React, { useContext } from "react";
import { UserDispatch } from "./App";

const User = React.memo(function User({ user }) {
  const { id, username, email, active } = user;
  const dispatch = useContext(UserDispatch);

  const onRemove = (id) => {
    dispatch({
      type: "REMOVE_USER",
      id,
    });
  };
  const onToggle = (id) => {
    dispatch({
      type: "TOGGLE_USER",
      id,
    });
  };
  return (
    <div>
      {console.log(`User 컴포넌트(${username})가 렌더링 되었습니다.`)}
      <b
        style={{
          color: active ? "green" : "black",
          cursor: "pointer",
        }}
        onClick={() => onToggle(id)}
      >
        {username}
      </b>
      <span>({email})</span>
      <button onClick={() => onRemove(id)}>삭제</button>
    </div>
  );
});

function UserList({ users }) {
  return users.map((user) => {
    return <User user={user} key={user.id} />;
  });
}

export default React.memo(UserList);

이렇게 User 컴포넌트에서 useContext라는 훅을 사용해서 UserDispatch 컨텍스트를 사용할 수 있습니다. 우리가 App 컴포넌트에서 UserDispatch를 export 해주었기 때문에 여기서 사용할 수가 있는 것입니다.

그리고 User 컴포넌트에서 dispatch를 이용해서 onToggle, onRemove를 여기서 정의 하면 됩니다.

 

useReducer와 useState의 차이를 발견

만약 이 예제에서 useReducer를 사용하지 않고 useState를 사용해서 내부에서 모든 것을 작업했더라면 dispatch가 없기 때문에 방금 했던 것 처럼 UserDispatch 같은 컨텍스트를 만들어서 관리하는게 조금 어려워질 수 있을 것입니다.

물론 Provider에 value로 setState 관련 함수를 넣어주는 방식으로 구현을 할 수는 있겠지만 방금 했던 것처럼 간단하고 깔끔하게 작성할 수는 없을 것입니다.

앞으로 특정 함수를 여러 컴포넌트에 거쳐서 전달해주어야 할 일이 있다면 방금한 것 처럼 dispatch를 관리하는 컨텍스트를 만들어서 필요한 곳에서 바로 dispatch를 바로 불러와서 사용하면 구조도 깔끔해지고 코드 작성도 쉬워집니다.

지금은 컨텍스트에 상태(state)는 넣지 않고 디스패치만 넣었는데 나중에 가서는 상태도 같이 넣는 방법을 배워보겠습니다.

 

728x90
반응형

'Front-end > React' 카테고리의 다른 글

[React] 리덕스 코드 작성하기  (0) 2021.06.09
[React] 리덕스 - 개념  (0) 2021.06.09
[React] Custom Hook 만들기  (0) 2021.06.07
[React] useReducer  (0) 2021.06.07
[React] react-router (Switch, Link, exact, render, useHistory)  (0) 2021.05.13