본문 바로가기

React.js

React(리액트): Context API를 사용한 전역 값 관리

반응형

이번에는 Context API를 사용해 리액트에서 전역적인 값을 관리하는 방법에 대해 알아보자.

 

App.js

const onToggle = useCallback(id => {
    dispatch({
      type: 'TOGGLE_USER',
      id
    });
  }, []);
  
  const onRemove = useCallback(id => {
    dispatch({
      type: 'REMOVE_USER',
      id
    });
  }, []);
  
  return (
  	<UserList users={users} 
    onToggle={onToggle} onRemove={onRemove}/>
    <div>활성 사용자 수: {count}</div>
    )

이전에 구현했던 App 컴포넌트를 살펴보면 App 컴포넌트 내부에서 'onToggle'과 'onRemove'가 구현된 다음,

이 두 가지 함수가 UserList 컴포넌트에게 전달이 되는 구조이다. 

 

UserList.js

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>
    );
}

UserList에서는 props로 onRemove와 onToggle을 받아와서 다시 User 컴포넌트에게 주고 있다.

User 컴포넌트를 보면 마찬가지로 props로 onRemove와 onToggle을 받아와 사용하고 있다.

 

 

여기서 UserList는 사실상 다리 역할만하고 있다. UserList에서 직접 onRemove와 onToggle을 사용하는 일은 없는데 

User에게 전달해주어야 하기 때문에 App 컴포넌트에서 UserList에게 onRemove와 onToggle을 설정해준 것이다.

그리고 UserList는 다시 User에게 전달해주는 구조이다.

이 구조는 지금 같은 상황의 경우는 크게 문제가 되지 않지만 컴포넌트의 구조가 더 복잡해진다면 문제가 될 수 있다.

이 문제를 해결하기 위해 Context를 사용할 수 있다.

 

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;

Context API를 학습해보기 위해 ContextSample.js라는 파일을 생성해주었다. 다음 코드는 ContextSample을 시작으로

위의 함수를 호출하며 Child 함수를 호출한다. 이를 Context API를 사용해서 한 번에 ContextSample에서 Child로 가도록

만들어주자. 

 

ContextSample.js

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

const MyContext = createContext('defaultVlaue');

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

function Parent() {
    return <Child />
}

function GrandParent() {
    return <Parent />
} 

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

다음과 같이 사용해줄 수 있다. 

먼저 createContext라는 함수를 사용해 Context에서 사용할 기본값을 넣어준다. 여기서는 'defaultValue'를 넣어주었다.

 

Child에서 6번 줄과 같이 MyContext에 있는 값을 불러와서 쓸 수 있다. useContext는 Context에 있는 값을 읽어와서 

사용할 수 있게 해주는 리액트에 내장된 Hook이다. 

 

MyContext 값을 지정해주고 싶다면 Provider를 사용해야 한다. 20번 줄을 보면 MyContext 안에 Provider라는 

컴포넌트가 있고 이를 사용해서 GrandParent를 감싸주고, value라는 값을 'GOOD'으로 넣어주었다.

이렇게 하고 난 뒤에는 각 함수의 props들을 지워줘도 된다.

 

지금까지의 구조를 다시 살펴보면

MyContext.Provider를 사용해 value값을 'GOOD'으로 설정해주었고 Context에 값이 설정되게 된다.

Child 컴포넌트에서는 useContext를 사용해 MyContext에 있는 값을 그대로 불러온다. 

불러온 값이 text에 저장되고 화면에 표시되도록 한다.  

 

MyContext와 같은 Context를 다른 파일에서도 작성할 수 있고, 작성한 다음 내보낸 것을 어디서든지 사용할 수 있다는 큰 장점이 있다. 

 

지금까지 학습한 내용을 다시 정리해보자.

Context를 만들 때는 createContext라는 함수를 사용하고 이 함수에 들어가는 값은 기본값이다.

이 기본값은 만약에 Provider라는 컴포넌트가 사용되지 않았을 때의 기본적인 값이고 그 값을 직접 설정하고 싶다면

MyContext 안에 있는 Provider라는 컴포넌트를 사용해서 value 값을 설정해주면 그 값이 Context의 값이 된다.

 

추가적으로 이 Context의 값은 유동적으로 변할 수 있다.

 

ContextSample.js

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

const MyContext = createContext('defaultVlaue');

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

function Parent() {
    return <Child />
}

function GrandParent() {
    return <Parent />
} 

function ContextSample() {
    const [value, setValue] = useState(true);
    return (
    <MyContext.Provider value ={value ? 'GOOD' : 'BAD'}>
        <GrandParent />
        <button onClick={() => setValue(!value)}>CLICK ME</button>
    </MyContext.Provider>
    )
}

export default ContextSample;

버튼을 누르면 value값이 유동적으로 변하게 만들어 주었다. useState를 통해 상태 관리를 한다. 

 

이렇게 Context를 사용하게 되면 깊은 곳에 있는 컴포넌트에게도 바로 어떤 값을 전달해줄 수 있다.

 

App 컴포넌트에서 이 Context를 사용해보자. Context API를 사용해서 기존에 작성했던 코드들을 수정해 줄 것이다.

 

'onRemove'와 'onToggle' 함수를 수정해 줄 건데, Context를 통해서 이 함수들을 직접 넣어줄 수도 있겠지만 그 대신 'dispatch'만 따로 넣어줄 것이다.

 

App.js

export const UserDispatch = createContext(null);

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

App 컴포넌트에 추가되는 코드들은 다음과 같다.

기본값은 필요없으니 null로 넣어주었고 UserDispatch 안에는 Provider라는 컴포넌트가 들어있는데, 

그 Provider 컴포넌트를 사용해주었다. value 값을 기존에 App 컴포넌트에 있던 dispatch를 넣어주면 된다.

우리는 UserDispatch라는 Context를 만들어주었고 기본값은 필요 없기 때문에 null로 넣어주었고 이 Context의 값은

value로 넣어준 useReducer를 통해 받아온 dispatch이다. 

 

이제는 User 컴포넌트에서 바로 'onRemove'와 'onToggle'을 사용하는 방법을 알아보자.

먼저 App 컴포넌트에 있는 onToggle과 onRemove 함수는 필요없으니 지워주자.

UserList 컴포넌트에 있는 onToggle과 onRemove도 모두 지워주자. User 컴포넌트에서 바로 만들어 줄 것이다.

 

UserList.js

const dispatch = useContext(UserDispatch)

User 컴포넌트에 다음과 같이 dispatch를 선언해주자.

App.js에서 UserDispatch를 불러오고 useContext의 파라미터로 넣어주었다. 이렇게 하면 dispatch를 사용할 수 있게 되고, onToggle과 onRemove를 구현해줄 수 있다.

*useContext는 Context를 컴포넌트 내부에서 바로 조회할 수 있게 해주는 Hook이다.

 

UserList.js

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

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

    )
})

기존에 App.js에서 우리가 만들었던 액션에 보면 TOGGLE_USER와 REMOVE_USER가 있었는데 이 액션을 User 컴포넌트에서 발생시켜주면 된다.

 

여기서 useState와 useReducer를 사용하는 것에서 차이점 하나를 발견할 수 있다.

만약 App.js에서 reducer를 사용하지 않고 useState를 사용해서 내부에서만 모든 것을 작업했다면 dispatch가 없기 

때문에 위에서 했던 것처럼 UserDispatch와 같은 Context를 만들어서 관리하는 게 조금 어려워질 수 있다.

물론 UserDispatch.Provider의 value에 'setState' 관련 함수를 넣어줘도 구현할 수 있긴 하겠지만 위에서 했던 것처럼 

깔끔한 구조는 아닐 것이다. 

 

만약 앞으로 함수를 여러 컴포넌트에 걸쳐서 전달해줘야 하는 작업이 있을 때 위에서 처럼 dispatch를 관리하는 

Context를 만들어 필요한 곳에서 dispatch를 불러 사용한다면 구조도 깔끔해지고 코드 작성하기도 쉬워진다.

이런 상황이 온다면 이번에 학습한 내용을 잘 적용시켜보자.

 

지금까지의 코드:

 

App.js

import React, { useRef, useReducer, useMemo, useCallback, createContext } 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 = {
  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() {
  const [{ username, email }, onChange, onReset] = 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
      }
    });
    onReset();
    nextId.current += 1;
  }, [username, email, onReset]);

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

export default App;

UserList.js

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

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

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

    )
})

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

export default React.memo(UserList,
    (prevProps, nextProsp) => nextProsp.users === prevProps.users
);

 

숙제

User 컴포넌트에게 따로 onToggle / onRemove 를 props로 전달하지 않고 바로 dispatch 를 사용했던 것처럼, CreateUser 컴포넌트에서도 dispatch 를 직접 하도록 구현을 해보세요.

  • CreateUser 에게는 아무 props 도 전달하지 마세요.
  • CreateUser 컴포넌트 내부에서 useInputs를 사용하세요.
  • useRef를 사용한 nextId 값을 CreateUser에서 관리하세요.

답:

App.js

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

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

const initialState = {
  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 = React.createContext(null);

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

  const { users } = state;

  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <UserDispatch.Provider value={dispatch}>
      <CreateUser />
      <UserList users={users} />
      <div>활성사용자 수 : {count}</div>
    </UserDispatch.Provider>
  );
}

export default App;

CreatUser.js

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

const CreateUser = () => {
  const [{ username, email }, onChange, reset] = useInputs({
    username: '',
    email: ''
  });

  const nextId = useRef(4);
  const dispatch = useContext(UserDispatch);

  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);
반응형