글 작성자: bbangson
반응형

Context API

 Context API의 Context는 컴포넌트의 단계마다 Props를 일일이 전달하지 않고, 원하는 컴포넌트 트리에 데이터를 제공할 수 있는 State Management 툴입니다. 

 

 React는 데이터를 상위에서 하위 컴포넌트로 Props를 전달하는 구조입니다. 그래서 구현된 컴포넌트 트리에 컴포넌트 개수가 많아질 경우에는 데이터를 전달하는 과정이 복잡해질 수 있습니다. Context API를 이용하면 각 컴포넌트마다 데이터를 파라미터로 받거나 하위 컴포넌트에 데이터를 전달하는 과정이 생략됩니다. 

 

즉, 이러한 관리를 전역적으로 상태를 관리한다고 말할 수 있습니다.

 

 Context API에는 Provider, createContext, Consumer의 개념이 있습니다. 이들의 개념만 알아도 Context API를 사용하기에는 무리가 없습니다. 그만큼 러닝커브도 낮기 때문에, 러닝 커브가 높은 Redux보다 요즘 다시 떠오르는 추세이기도 합니다. 

 

 

createContext

- context 객체를 만듭니다. 이 함수를 호출하면 ProviderConsumer 컴포넌트를 사용할 수 있습니다. 

 

 

Provider

- Provider 컴포넌트를 통해서 Context의 값을 설정할 수 있습니다. 값을 설정하고 전달하고 싶은 컴포넌트 트리의 최상단에서 감싸주면 됩니다. 

 

 

Consumer

- context의 변화를 구독하는 컴포넌트입니다. 이 컴포넌트를 사용하면 설정된 context를 사용할 수 있습니다. 좀더 간편하게 사용하기 위해서 React Hook에 내장된 useContext를 사용하여도 좋습니다. 

 

 

 

 

간단한 예시를 통해 배우는 Context API

 

만약 전역상태 관리 없이 State를 전달하고자 한다면 이런 식의 코드가 될 겁니다. 

 

 

ContextSample.js

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

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

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

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

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

export default ContextSample;

 ContextSample 함수는 GrandParent를 렌더링 하면서 "GOOD" 이 저장된 text를 하위 컴포넌트인 GrandParent에 전달하고 있습니다. GrandParent 함수 또한, 상위 컴포넌트에서 받아온 text를 하위 컴포넌트에 전달합니다. 최종적으로 Child 컴포넌트에 있는 <div> 태그를 화면에 렌더링 하게 되는데, 결국 ContextSample()에서 전달받은 "GOOD"이라는 단어가 화면에 렌더링 될 것입니다. 

 

Context API를 사용하지 않고 전달받은 Props 렌더링하는 모습

 하나의 파일 안에서 조그만한 함수로 예를 들어서, 별 거 없어 보일 수도 있습니다. 하지만 관리해야 할 파일이 많아지고 

이 파일들이 전역 State를 공유하고 있다면 어떻게 될까요? 하나하나 파일을 보면서 Props를 잘 전달하는지 확인해야 되고 State 관리하기도 어려워질 것입니다. 이를 예방하기 위해 전역 상태 관리 툴을 사용하는 것이며, 그중 하나가 Context API입니다. 근데 넌 

 

 

 Context API를 사용하여 위의 예제를 바꿔보겠습니다. 

import React, { createContext, useContext, useState } from 'react';

// Context를 만들때는 createContext를 사용한다. 
const MyContext = createContext('defaultValue');

function Child() {
  // useContet는 context의 값을 읽어와서 사용할 수 있게 해주는 React에 내장된 Hook이다. 
  const text = useContext(MyContext);
  return <div> 안녕하세요? {text} </div>;
}

function Parent() {
  return <Child/>;
}

function GrandParent() {
  return <Parent/>;
}

function ContextSample() {
  const [value, setValue] = useState(true); 
  
  return (
    // Provider를 통해서 value를 설정하여 MyContext의 값을 설정할 수 있다. 
    <MyContext.Provider value={value ? "Good" : "Bad"}>
      <GrandParent/>
      <button onClick={() => setValue(!value)}>Click Me!</button>
    </MyContext.Provider>
  );
}

export default ContextSample;

 createContext 함수로 Context(MyContext)를 생성합니다. 가장 상위 컴포넌트인 ContextSample() 함수에서 Provider를 통해서 value 값을 설정할 수 있고, 이를 통해 MyContext의 값이 설정됩니다. createContext()로 만든 Context는 다른 파일에서 선언할 수 있습니다. 즉, 다른 파일에서 작성된 Context는 쉽게 불러와서 언제든지 사용할 수 있다는 것이 Context API의 장점입니다. 

 

 최상위 컴포넌트에서는 Provider를 통해 Context의 값을 선언하고 하위 컴포넌트로 해당 값을 전달해주는 역할을 합니다. 이 Context의 값을 사용하기 위해서는 useContext Hook을 이용하여 사용할 수 있습니다. useContext는 Context의 값을 읽어와서 사용할 수 있게 해주는 React에 내장된 훅입니다.

 

Context API를 사용한 결과 화면

 

 

 

 

 

 

TodoList에 적용해보는 Context API

 

 지금까지 진행한 기존 코드를 Toggle로 보여드리겠습니다.

더보기
import React, { useRef, useMemo, useCallback, useReducer } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
import useInputs from './useInputs';

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter((user) => user.active).length;
}

const initialState = {
  inputs: {
    username: '',
    email: '',
  },
  users: [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true,
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false,
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false,
    },
  ],
};

function reducer(state, action) {
  switch (action.type) {
    case 'CREATE_USER':
      return {
        users: state.users.concat(action.user),
      };
    case 'TOGGLE_USER':
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.id ? { ...user, active: !user.active } : user
        ),
      };
    case 'REMOVE_USER':
      return {
        ...state,
        users: state.users.filter((user) => user.id !== action.id),
      };
    default:
      return state;
  }
}

function App() {
  const [{ username, email }, onChange, reset] = useInputs({
    username: '',
    email: '',
  });
  const [state, dispatch] = useReducer(reducer, initialState);
  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 onToggle = useCallback((id) => {
    dispatch({
      type: 'TOGGLE_USER',
      id,
    });
  }, []);

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

  const count = useMemo(() => countActiveUsers(users), [users]);
  
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성 사용자 수 : {count}</div>
    </>
  );
}

export default App;
더보기
import React from "react";

const User = React.memo(function User({ user, onRemove, onToggle }) {
  const { username, email, id, active } = user;

  return (
    <div>
      <b
        style={{
          color: active ? "green" : "black",
          cursor: "pointer",
        }}
        onClick={() => onToggle(id)}
      >
        {username}
      </b>
      &nbsp;
      <span>({email})</span>
      <button onClick={() => onRemove(id)}>삭제</button>
    </div>
  );
});

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

export default React.memo(UserList);
더보기
import React from "react";

function CreateUser({ username, email, onChange, onCreate }) {
  console.log('CreateUser');
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
}

export default React.memo(CreateUser);
더보기
import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);

  // change
  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    setForm((form) => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);

  return [form, onChange, reset];
}

export default useInputs;

 

 위의 코드는 useReducer Hook 함수를 이용하여 state 관리까지 되어있습니다. 

위에서 수정된 코드는 주석으로 설명하겠습니다. 

 

 

App.js

import React, {
  useMemo,
  useReducer,
  createContext,
} from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter((user) => user.active).length;
}

const initialState = {
  inputs: {
    username: '',
    email: '',
  },
  users: [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true,
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false,
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false,
    },
  ],
};

function reducer(state, action) {
  switch (action.type) {
    case 'CREATE_USER':
      return {
        users: state.users.concat(action.user),
      };
    case 'TOGGLE_USER':
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.id ? { ...user, active: !user.active } : user
        ),
      };
    case 'REMOVE_USER':
      return {
        ...state,
        users: state.users.filter((user) => user.id !== action.id),
      };
    default:
      return state;
  }
}

// UserDispatch 생성
export const UserDispatch = createContext(null);

function App() {
  // onCreate, onToggle 삭제

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

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

  return (
    {/*dispatch(action) 값을 UserDispatch Context의 값으로 설정하고 하위 컴포넌트에 전달.*/}
    <UserDispatch.Provider value={dispatch}>
      {/*CreateUser 컴포넌트에 아무 Props도 전달하지 않음*/}
      <CreateUser/>
      {/*UserList 컴포넌트에 onRemove, onToggle 함수를 전달하지 않음*/}
      <UserList users={users} />
      <div>활성 사용자 수 : {count}</div>
    </UserDispatch.Provider>
  );
}

export default App;

 

 

UserList.js

import React, { useContext } from 'react';
// App.js에서 UserDispatch Context를 import함
import { UserDispatch } from './App';

const User = React.memo(function User({ user }) {
  const { username, email, id, active } = user;
  
  // App.js에 있는 useReducer의 dispatch를 useContext Hook을 이용하여 사용할 수 있다.
  const dispatch = useContext(UserDispatch);

  return (
    <div>
      <b
        style={{
          color: active ? 'green' : 'black',
          cursor: 'pointer',
        }}
        {/*dispath로 useReducer에 있는 action을 이용한다.*/}
        onClick={() => dispatch({ type: 'TOGGLE_USER', id })}
      >
        {username}
      </b>
      &nbsp;
      <span>({email})</span>
      {/*dispath로 useReducer에 있는 action을 이용한다.*/}
      <button onClick={() => dispatch({ type: 'REMOVE_USER', id })}>
        삭제
      </button>
    </div>
  );
});

// 하위 컴포넌트인 User 함수에 onRemove, onToggle 함수를 전달할 필요가 없다.
function UserList({ users }) {
  return (
    <div>
      {users.map((user) => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );
}

export default React.memo(UserList);

 

 

CreateUser.js

import React, { useContext, useRef } from 'react';
import { UserDispatch } from './App';
import useInputs from './useInputs';

// 기존에는 파라미터로 username, email, onChange, onCreate를 받아왔지만,
// Context API를 사용하기 때문에 받아올 필요가 없다.
function CreateUser() {
  console.log('CreateUser');
  // useContext로 받아온 Context의 값을 사용한다. 
  const dispatch = useContext(UserDispatch);
  const [{ username, email }, onChange, reset] = useInputs({
    username: '',
    email: '',
  });
  const nextId = useRef(4);
  
  // App.js에 있던 기존 onCreate 함수를 CreateUser 컴포넌트에서 관리한다. 
  const onCreate = () => {
    dispatch({
      type: 'CREATE_USER',
      user: {
        id: nextId.current,
        username,
        email,
      },
    });
    reset();
    nextId.current += 1;
  };

  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
}

export default React.memo(CreateUser);

 

 

반응형