Context API

gyomni·2022년 2월 27일
0

React

목록 보기
9/9
post-thumbnail

Context API

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

특정 컴포넌트를 거쳐서 원하는 컴포넌트에게 전달

App.js

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

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

const initialState = {
  users: [
    {
      id:1,
      username: 'gyomni',
      email: 'hi1@gmail.com',
      active:true,
  },
  {
      id:2,
      username: 'joy',
      email: 'hi2@gmail.com',
      active:false,
  },
  {
      id:3,
      username: 'zoe',
      email: 'hi3@gmail.com',
      active:false,
  }
  ]
};

function reducer(state, action) {
  switch (action.type) {
    case 'CREATE_USER':
      return {
        users: state.users.concat(action.user)
      };
    case 'TOGGLE_USER':
      return {
        users: state.users.map(user =>
          user.id === action.id ? { ...user, active: !user.active } : user
        )
      };
    case 'REMOVE_USER':
      return {
        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} onToggle={onToggle} onRemove={onRemove} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

-> App 컴포넌트 내부에서 onToggle, onRemove가 구현된 다음에 이 두가지 함수가 UserList에게 전달이 된 상태.

UserList.js

import React, { useEffect } from "react";

const User = React.memo(function User({user, onRemove,onToggle}){
    const {username, email, id, active} =user;
    useEffect(()=>{
        console.log('user값이 설정됨');
        console.log(user);
        return()=>{
            console.log('user 값이 바뀌기전');
            console.log(user);
        }
    },[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}){ // users배열을 props로 받아오기

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

export default React.memo(UserList);
  • UserList 컴포넌트에서는 PropsonRemove, onToggle를 받아와서 이 값을 User에게 주고 있다.
  • UserList 컴포넌트는 일종의 다리 역할만 하고 있음.
    ( UserList 컴포넌트에서는 직접적으로 onRemove, onToggle를 사용하는 일은 없는데, User컴포넌트에게 전달해 줘야 하기 때문에 App컴포넌트에서 onRemove, onToggleUserList 에게 설정해 준 것.
    그리고 UserListUser컴포넌트에게 전달해준 것.)

특정 함수를 특정 컴포넌트를 거쳐서 원하는 컴포넌트에게 전달하는 작업은 리액트로 개발을 하다보면 자주 발생 할 수 있는 작업이다.
위와 같은 상황 처럼 컴포넌트 한개 정도를 거쳐서 전달하는 것은 문제가 되지 않는다.

But! 컴포넌트의 구조가 좀 더 복잡해졌다면? 😵

이렇게 Props가 컴포넌트를 거쳐서 거쳐서 전달한다면 매우 번거로울 것이다.


코드로 확인해 보자면,

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에서 설정한 text="Good" -> GrandParent{text}에서 text ={text}로 넘겨주고 -> Parent{text}에서 text ={text}로 넘겨주고 -> Child{text}에서 안녕하세요? {text}가 보여지게 됨.

  • 여기서 원하는 루트는 ContextSample에서 바로 Child{text}로 바로 넘겨주는 것.
    => Context API를 사용해보자!

컴포넌트의 구조가 복잡할땐 리액트의 Context APIdispatch 를 함께 사용하면 해결 할 수 있다! 🤩



💡 Context API 사용

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

const MyContext =React.createContext('defaltValue');  // context에서 사용할 기본값 넣어줌.

function Child(){ 
    const text = useContext(MyContext); // MyContext의 값을 불러와서 사용. 
    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;

  • useContext : context에 있는 값을 읽어와서 사용할 수 있게 해주는 리엑트에 내장된 hook.
    ( context를 컴포넌트 내부에서 바로 조회할 수 있는 hook. )
  • React.createContext() : context를 만들 때 사용하는 함수.
    -> 파라미터 : 기본 값
    -> 기본 값은 Provider컴포넌트가 사용되지 않을 때의 기본적인 값.
  • context의 value를 지정해 주지 않았기 때문에 defaltValue가 나타가게됨.


MyContext의 값을 지정해주고 싶다면?
=> ContextSample에서 MyContext안에 있는 Provider라는 컴포넌트를 사용해서 value값을 설정해주면 된다 !


💡 Provider 사용

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

const MyContext =React.createContext('defaltValue');

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;

  • MyContext안의 Provider 컴포넌트를 사용해서 GrandParent를 감싸준다.
  • MyContext.Provider를 통해 value를 설정해줬다. = context의 값이 설정된다.
  • 사용하지 않는 컴포넌트의 {text}를 지워준다.
  • Child 컴포넌트에서 useContext사용해서 MyContext에 있는 값을 그대로 불러와서 text값을 사용할 수 있게 됬다.
  • MyContext와 같은 context를 다른 파일에서도 작성할 수 있다.
    -> 다른 파일에서 작성한 다음 내보낸 것을 불러와서 어디서든지 사용할 수 있다는 장점이 있다.

context 값은 유동적으로 변할 수 있다! => useState사용해서 변경하기.


💡 useState 사용

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

const MyContext =React.createContext('defaltValue'); 

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;

  • 값이 바뀔 때 마다 단계적(ContextSample->GrandParent->Parent->Child)으로 값을 전달하는 것이 아니라, Provider을 통해서 MyContext의 값을 특정 값으로 설정해주면 Child에서 바로 MyContext 참조해서 쓸 수 있다.

context를 사용하게 된다면 깊은 곳에 있는 컴포넌트에 바로 넣어줄 수 있게 된다!

문제 해결하기 & 코드 파헤치기

문제 상황 : 상단의 App.js를 보면 onToggle ,onRemove이 두가지 함수를 User컴포넌트에 주기 위해서 UserList컴포넌트를 거쳐서 줘야 한다.
-> context를 통해서 onToggle ,onRemove를 직접 넣어 줄 수도 있겠지만, 그 대신 dispatch만 따로 넣어주는 것으로 해결해보자!


App.js

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

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

const initialState = {
  users: [
    {
      id:1,
      username: 'gyomni',
      email: 'hi1@gmail.com',
      active:true,
  },
  {
      id:2,
      username: 'joy',
      email: 'hi2@gmail.com',
      active:false,
  },
  {
      id:3,
      username: 'zoe',
      email: 'hi3@gmail.com',
      active:false,
  }
  ]
};

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

export const UserDispatch = React.createContext(null); // 기본값 필요없으므로 null 넣어줌.

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

export default App;

📍 createContext 불러오기

import React, { useRef, useReducer, useMemo, useCallback, createContext } from 'react';

📍 UserDispatch안의 Provider컴포넌트 사용하기


  return (
    <UserDispatch.Provider value = {dispatch}>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onToggle={onToggle} onRemove={onRemove} />
      <div>활성사용자 수 : {count}</div>
    </UserDispatch.Provider>
  );
  • App 컴포넌트에서 reducer통해서 받아온 dispatchvalue로 넣어준 것.
    const [state, dispatch] = useReducer(reducer, initialState); 

UserList

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

const User = React.memo(function User({user}){
    const {username, email, id, active} =user; 
    const dispatch = useContext(UserDispatch);
   
    useEffect(()=>{ 
        console.log('user값이 설정됨');
        console.log(user);
        return()=>{
            console.log('user 값이 바뀌기전');
            console.log(user);

        }
    },[user]); 
    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}){ // users배열을 props로 받아오기

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

export default React.memo(UserList);

📍 useEffect 불러오기

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

📍 dispatch 사용

import { UserDispatch } from "./App";
.
.
.
 const dispatch = React.useContext(UserDispatch);
  • 파라미터에는 App에서 만든 UserDispatch 넣어주기.
    App에서 UserDispatchexport해줬기 때문에 상단에서 불러올 수 있음.

📍 onToggle , onRemove 구현

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컴포넌트에서 만든 action을 보면 TOGGLE_USERREMOVE_USER가 있었다 !
    이것을 User컴포넌트에서 바로 사용.

useStste를 사용하는 것과 useReduce를 사용하는 것의 차이점 발견 👓

만약 App컴포넌트에서 reducer을 사용하지 않고 useState를 사용해서 내부에서 모든 것을 작업했더라면 dispatch가 없기 때문에 UserDispatch같은 context를 만들어서 관리하는 것이 조금 어려워 질 수 있다.

물론, App컴포넌트의 UserDispatch.Providervalue에다가 setState관련 함수를 넣어주는 방식으로 구현가능하지만, 덜 깔끔한 구조가 된다.

앞으로 특정 함수를 여러 컴포넌트에 거쳐서 전달해줘야 하는 일이 있다면?

dispatch를 관리하는 context를 만들어서 필요한 곳에서 dispatch를 불러와서 사용하면 굉장히 구조도 깔끔해지고, 코드 작성도 쉬워진다.

학습 : 벨로퍼트와 함께 하는 모던 리엑트

profile
Front-end developer 👩‍💻✍

0개의 댓글