Context API 를 사용한 전역 값 관리

Hyun·2021년 10월 2일
0

리액트 기초

목록 보기
14/18

아래 코드를 보면 App 컴포넌트에서 onToggle, onRemove, onModify가 구현되어 있고, 이 함수들은 UserList 컴포넌트를 거쳐 각 User 컴포넌트들에게 전달되고 있다.

여기서 UserList 컴포넌트의 경우, 해당 함수들을 직접 사용하지 않고 함수들을 전달하기 위한 중간다리 역할만 하고 있다.

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

위와 같이 특정 함수를 특정 컴포넌트를 거쳐 원하는 컴포넌트에게 전달하는 작업은 리액트로 개발할 때 자주 발생할 수 있는 일이다. 크게 불편하진 않지만 3~4개 이상의 컴포넌트를 거쳐 전달을 해야하는 일이 발생하게 된다면, 이는 매우 번거로울 것이다.

그럴 땐, 리액트의 Context API 와 이전 글에서 다뤘던 dispatch 를 함께 사용하면 이러한 복잡한 구조를 해결할 수 있다.

리액트의 Context API 를 사용하면 프로젝트 안에서 전역적으로 사용할 수 있는 값을 관리할 수 있다. 이 값은 꼭 상태를 가르키지 않아도 된다. 함수일수도 있고, 어떤 외부 라이브러리 인스턴스일수도 있고, 심지어 DOM 일 수도 있다.

물론 Context API 를 사용해서 프로젝트의 상태를 전역적으로 관리할 수도 있다. 이는 나중에 자세히 다뤄보도록 하겠다.

Context 를 만들 땐, 다음과 같이 React.createContext() 라는 함수를 사용한다.

const UserDispatch = React.createContext(null);

createContext 의 파리미터에는 Context 의 기본값을 설정할 수 있다. 여기서 설정하는 값은 Context 를 쓸때 값을 따로 지정하지 않을 경우 사용되는 기본(초기)값이다.

Context 를 만들면 Context 안에 Provider 라는 컴포넌트가 들어있는데, 이 컴포넌트를 통하여 Context 의 값을 정할 수 있다. 이 컴포넌트를 사용할 때는 value 라는 값을 설정해주면 된다.

<UserDispatch.Provider value={dispatch}>...</UserDispatch.Provider>

이렇게 설정해주고 나면 Provider 에 의하여 감싸진 컴포넌트 중 어디서든지 Context 의 값을 바로 조회해서 사용할 수 있다.

Context 를 만들고 내보내주었다. 그리고 dispatch 를 Context 의 값으로 설정해주었다.

App.js

import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';

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

const initialState = {
  input: {
    username: '',
    email: '',
    id: ''
  },
  users: [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: true
    },
    {
      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),
        input: {
          ...state.input,
          username: '',
          email: ''
        }, //input 값 초기화

      }
    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)
      }  
    case 'MODIFY_USER':
      return {
        ...state,
        input: {
          username: action.username,
          email:action.email,
          id: action.id
        }
      }
    case 'UPDATE_USER':
      return {
        ...state,
        users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
        input: {
          ...state.input,
          username: '',
          email: ''
        }
      }  
      default:
        return state;
  }
}

export const UserDispatch = React.createContext(null); 
// Point!, Context 를 만들고 내보내주었다.

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const nextId = useRef(4);

  const { users } = state;
  const { username, email } = state.input;
  
  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    dispatch({
      type: 'CHANGE_INPUT',
      name,
      value
    })
  }, []);//재선언할 필요가 없는 함수이다.

  const onCreate = useCallback(() => {
    dispatch({
      type: 'CREATE_USER',
      user: {
        id: nextId.current,
        username: username,
        email: email
      },
    })

    nextId.current += 1;
  }, [username, email])
  //여기서 (...)users를 사용해 user를 업데이트하는게 아니기 때문에 deps에 없음! 

  const onToggle = useCallback((id)=> {
    dispatch({
      type: 'TOGGLE_USER',
      id: id
      //굳이 아래처럼 안해도 됨!
      //user: {
      //  id: id
      // }
    })
  }, [])

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

  const onModify = useCallback(({username, email, id})=> {
    dispatch({
      type: 'MODIFY_USER',
      username: username,
      email: email,
      id: id
    })
  }, [])

  const onUpdate = useCallback(()=>{
    dispatch({
      type: 'UPDATE_USER'
    })
  }, [])

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

  return (
    <UserDispatch.Provider value={dispatch}> 
    // Point!, 태그로 감싼 부분의 어디서든 Context value 인 dispatch 를 조회하여 사용할 수 있다.
      <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate} />
      <UserList users={users} onToggle={onToggle} onRemove={onRemove} onModify={onModify}/>
      <div>활성사용자 수 : {count}</div>
    </UserDispatch.Provider>
  );
}

export default App;

Context 를 다 만든 후, App 에서 onToggleonRemove, onModify 를 지우고 UserList 에게 props 로 전달하는 것도 지웠다.

import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';

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

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

function reducer(state, action){
  switch (action.type){
    case 'CHANGE_INPUT':
      return {
        ...state,
        input: {
          ...state.input,
          [action.name]: action.value
        }
      }
    case 'CREATE_USER':
      return {
        users: state.users.concat(action.user),
        //concat() 메서드는 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.
        input: {
          ...state.input,
          username: '',
          email: ''
        }, //input 값 초기화

      }
    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)
      }  
    case 'MODIFY_USER':
      return {
        ...state,
        input: {
          username: action.username,
          email:action.email,
          id: action.id
        }
      }
    case 'UPDATE_USER':
      return {
        ...state,
        users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
        input: {
          ...state.input,
          username: '',
          email: ''
        }
      }  
      default:
        return state;
  }
}

export const UserDispatch = React.createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const nextId = useRef(4);

  const { users } = state;
  const { username, email } = state.input;
  
  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    dispatch({
      type: 'CHANGE_INPUT',
      name,
      value
    })
  }, []);//재선언할 필요가 없는 함수이다.

  const onCreate = useCallback(() => {
    dispatch({
      type: 'CREATE_USER',
      user: {
        id: nextId.current,
        username: username,
        email: email
      },
    })

    nextId.current += 1;
  }, [username, email])
  //여기서 (...)users를 사용해 user를 업데이트하는게 아니기 때문에 deps에 없음!

  const onUpdate = useCallback(()=>{
    dispatch({
      type: 'UPDATE_USER'
    })
  }, [])

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

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

export default App;

이후 User 컴포넌트에서 Context 를 조회하였고 Context 의 값을 사용해주었다.
( Context 의 값을 사용하기 위해서는 useContext 라는 hook 이 필요하다. )

UserList.js

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

const User = React.memo(function User({user}){
  const dispatch = useContext(UserDispatch);

  return(
    <div>
      <b
      style={{
        cursor: 'pointer',
        color: user.active ? 'green' : 'black'
      }} 
      onClick={()=> {
        dispatch({ type: 'TOGGLE_USER', id: user.id });
      }}>{user.username}</b> <span>{user.email}</span>
      <button onClick={() => {
        dispatch({ type: 'REMOVE_USER', id: user.id})
      }}>삭제</button>
      <button onClick={() => {
        dispatch({ type: 'MODIFY_USER', username: user.username, email: user.email, id: user.id})
      }}>수정</button>
    </div>
  )
})

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

export default React.memo(UserList);	

추가로 CreateUser 컴포넌트에서도 전역값인 dispatch 를 바로 사용할 수 있도록 구현하였다. props 로 username 과 email 을 전달하는 건 input 이 꼭 App 컴포넌트에 있어야 하기 때문에 어쩔 수 없다.

App 에서 onUpdateonCreate, onChange 를 지우고 CreateUser 에게 전달하는 props 도 지웠다.

App.js

import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';

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

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

function reducer(state, action){
  switch (action.type){
    case 'CHANGE_INPUT':
      return {
        ...state,
        input: {
          ...state.input,
          [action.name]: action.value
        }
      }
    case 'CREATE_USER':
      return {
        users: state.users.concat(action.user),
        //concat() 메서드는 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.
        input: {
          ...state.input,
          username: '',
          email: ''
        }, //input 값 초기화
      }
    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)
      }  
    case 'MODIFY_USER':
      return {
        ...state,
        input: {
          username: action.username,
          email:action.email,
          id: action.id
        }
      }
    case 'UPDATE_USER':
      return {
        ...state,
        users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
        input: {
          ...state.input,
          username: '',
          email: ''
        }
      }  
      default:
        return state;
  }
}

export const UserDispatch = React.createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const nextId = useRef(4);

  const { users } = state;
  const { username, email } = state.input;

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

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

export default App;

이후 CreateUser 컴포넌트에서 Context 를 조회하였고 Context 의 값을 사용해주었다.

CreateUser.js

before)

import React from "react";

function CreateUser({username, email, onChange, onCreate, onUpdate}){
  return(
    <div>
      <input 
      name="username"
      placeholder="계정명"
      onChange={onChange}
      value={username}
      />
      <input 
      name="email"
      placeholder="이메일"
      onChange={onChange}
      value={email}
      />
      <button onClick={onCreate}>등록</button>
      <button onClick={onUpdate}>업데이트</button>
    </div>
  )
}

export default CreateUser;

after)

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

function CreateUser({username, email}){
  const dispatch = useContext(UserDispatch);
  const nextId = useRef(4); 

  return(
    <div>
      <input 
      name="username"
      placeholder="계정명"
      onChange={(e)=>{
        const {name, value} = e.target;
        dispatch({ type: 'CHANGE_INPUT', name, value})
      }}
      value={username}
      />
      <input 
      name="email"
      placeholder="이메일"
      onChange={(e)=>{
        const {name, value} = e.target;
        dispatch({ type: 'CHANGE_INPUT', name, value})
      }}
      value={email}
      />
      <button onClick={()=>{
        dispatch({ type: 'CREATE_USER', user: { 
          id: nextId.current, 
          username: username, 
          email: email}
          });
        nextId.current += 1;  
      }}>등록</button>
      <button onClick={() => {
        dispatch({ type: 'UPDATE_USER' });
      }}>업데이트</button>
    </div>
  )
}

export default React.memo(CreateUser);

정리

useReducer 를 사용할 때 이렇게 dispatch 를 Context API 를 이용하여 전역적으로 사용할 수 있게 해주면, 컴포넌트에게 함수를 전달해야 하는 상황에서 코드의 구조가 훨씬 깔끔해질 수 있다. 예를 들어 깊은 곳에 위치하는 컴포넌트에게 여러 컴포넌트를 거쳐 함수를 전달해야 하는 상황에서 Context API 를 사용하면 좋을 것이다.

참고
props 로 변수를 전달하는 경우 https://jcon.tistory.com/176

profile
better than yesterday

0개의 댓글