13_리액트_Context API,immer
FrontEnd/React

13_리액트_Context API,immer

728x90

Context API

 

이 전글의 예제에서도 그렇지만 리액트에서 여러개의 컴포넌트를 거쳐가면서 Props를 전달해줘야 하는 경우가 많다.

 

즉, A -> B -> C -> D 와같이 차근차근 Props를 넘겨주는 경우인데, 이를 A -> D로 한번에 넘길 수 있는 방법이 있다.

 

//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 = "나는정민규" />
}


export default ContextSample;

다음과 같이  새 파일을 하나 만들고 보자.  현재 구조를 보면 Props가 차근차근 전달되는것을 알 수 있다. 이를 한번에 전달하는것으로 바꾸어 보자.

 

 

//ContextSample.js
import React, {createContext, useContext , useState} from 'react';

const MyContext = createContext('defaultValue');

function Child( ) {
    const text = useContext(MyContext) //MyContext값을 가져오는 Hook.
    return <div>안녕하세요? {text} </div>
}

function Parent() {
    return <Child />
}

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

function ContextSample() {
    const [value,setValue] = useState(true);
    return (
        <MyContext.Provider value = {value ? '나는정민규' : '나는저미규'}> { /* value값 설정 */ }
            <GrandParent  />
            <button onClick= { () => setValue(!value)}>클릭</button>
        </MyContext.Provider>
    )
}


export default ContextSample;

다음과 같은 코드를 보면 Provider를통해 value값을 정해주고, 이렇게 저장된 MyContext의 값을 useContext를 통해서 불러오는 것을 볼 수 있다. 하나의 전역변수처럼 사용된다는 의미인데, 위 예제처럼 유동되는 값으로 설정할 수도 있다.

 

 

위 예제를 그대로 계속 사용했던 예제에 적용해 보자.

 

CreateUser 에게는 아무 props 도 전달하지 말것.

CreateUser 컴포넌트 내부에서 useInputs 를 사용할것.

useRef 를 사용한 nextId 값을 CreateUser 에서 관리할것.

 

위 3개의 조건을 충족시키도록 코드를 변형해 보았다.

 

App.js

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



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

const initialState = {
  
  users : [
    {
        id : 1,
        username : 'mingyu',
        email : 'mingyu@naver.com',
        active : true  //항목 수정을 위해 추가
    },
    {
        id : 2,
        username : 'mingyu2',
        email : 'mingyu2@naver.com',
        active : false
    },
    {
        id : 3,
        username : 'mingyu3',
        email : 'mingyu3@naver.com',
        active : false
    }
  
  ]
}


function reducer(state,action) {
  switch (action.type){
    
    case 'CREATE_USER':
      return {
        inputs : initialState.inputs, //inputs를 날리는 작업
        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:
      throw new Error('unhandled action');
  }
}


export const UserDispatch = createContext(null);  // dispatch를 내보내는 식으로 적용


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;

CreateUser.js

더보기
//CreateUser.js

import React, { useCallback, useContext,useRef } from 'react';
import { UserDispatch } from './App';  //App에서 Dispatch를 가져옴
import useInputs from './useInputs';


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

    const dispatch = useContext(UserDispatch); // 가져온 Dispatch값을 불러옴
    const nextId = useRef(4); // 3까지는 적어두었기 때문


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


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

export default React.memo(CreateUser);

 

즉, App.js에 있던 항목들을 안으로 옮기는 과정을 수행했다고 생각하면 될 것 같다. 

 

 

 

immer

 

immer를 사용하면 불변성을 더 지키기 쉽다.

 

const object = {
    a : 1,
    b : 2
};

const nextobject = {
    ...object,
    b : 3
};

위처럼 객체를 바꾸어줘야 불변성이 잘 지켜지게 된다.

 

이때 스프레드연산자(...) 를 넣어도 되지만 코드가 복잡해지는 경우 좀 어려워진다.

 

 

즉, immer를 사용하면 불변성을 해치는 코드를 작성하더라도 스스로 해준다.

 

먼저 터미널 창을열어서

 

yarn add immer

를통해 immer을 설치해준다.

 

 

이제 App.js 파일에

 

import produce from 'immer'


window.produce = produce;

다음과 같은 줄을 추가해서 개발자옵션 콘솔 칸에서 Produce의 역할을 간단히 살펴보겠다.

 

const state = {
    number : 1,
    dontChangeMe : 2
};

 

const nextState = produce (state,draft => {
    draft.number +=1;
});

 

위와같이 상태를 바꾸어 준다면

 

nextState
// {number: 2, dontChangeMe: 2}

상태가 바뀐 것을 알 수 있다.

콘솔창 화면

두번째 파라미터인 draft안에서 값을 바꾸면 알아서 불변성을 지켜주게 된다.

 

 

const array = [
    { id : 1, text : 'mingyu'},
    { id : 2, text : 'mingyu2'},
    { id : 3, text : 'mingyu3'}
]
//undefined
const nextarray = produce(array,draft =>{
    draft.push({ id : 4, text : 'mingyu4' });
    draft[0].text = draft[0].text + 'hi';
});
//undefined


nextarray

nextarray 결과값

위 역시 안의 내용이 잘 바뀐 것을 확인할 수 있다.

 

 

자 그렇다면 App.js안의 아래 항목들을 리듀서 immer로 바꾸어 보자.

 

case 'CREATE_USER':
      return {
        inputs : initialState.inputs, //inputs를 날리는 작업
        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)
      }

아래처럼 바뀔 수 있다.

function reducer(state,action) {
  switch (action.type){
    
    case 'CREATE_USER':

      return produce(state,draft => { //immer 사용
        draft.users.push(action.user);
      })
     
    case 'TOGGLE_USER':


      return produce(state,draft => {
        const user = draft.users.find(user => user.id === action.id);
        user.active = !user.active;
      })
      
    case 'REMOVE_USER':
      return produce(state,draft => {
        const index = draft.users.findIndex(user => user.id === action.id);
        draft.users.splice(index,1);
      })
    
      // return{
      //   ...state,
      //   users : state.users.filter(user => user.id !== action.id)
      // }
    default:
      throw new Error('unhandled action');
  }
}

로직이 그렇게 복잡하지 않은 경우에는 굳이 사용할 필요가 없다.

 

 

 

 

 

 

 

이전에 아래와 같이 함수형 업데이트를 사용하는 법에 공부한적이 있다.

const [todo,setTodo] = useState({
	text : 'Hello',
    done : false
});

const onClick = useCallback(() => {
	setTodo(todo => ({
    	...todo,
        done : !todo.done
    }));
}, [] );

 이경우 immer를 사용하면 보다 간편하게 적을 수 있다.

 

immer에는 아래와 같은 성질이 있다.

 

 

const [todo,setTodo] = useState({
	text : 'Hello',
    done : false
});

const updater = produce(draft => {
	draft.done = !draft.done;
});

const nextTodo = updator(todo);

console.log(nextTodo);
// {text : 'Hello', done : true }

위처럼 produce에 첫번째 파라미터를 넣지 않는 경우 updater함수 자체를 반환하게 된다.

 

const [todo,setTodo] = useState({
	text : 'Hello',
    done : false
});

const onClick = useCallback(() => {
	setTodo(
    	produce(draft => {
        	draft.done = !draft.done;
        })
    );
}, []);

즉 위처럼 produce를 바로 함수형 파라미터 에 넣어줄 수 있다.

 

데이터가 엄청 많지 않으면 immer와 일반 코드의 속도차이가 별로 없다는 것을 알고 있으면 될 것 같다. 그래도 속도지연이 있으니 필요한 곳에만 사용하는것이 좋다.

728x90

'FrontEnd > React' 카테고리의 다른 글

16_리액트_컴포넌트스타일링  (0) 2021.12.24
15_리액트_유용한 tool  (0) 2021.12.24
12_리액트_여러 Hook들  (0) 2021.12.23
11_리액트_배열 랜더링  (0) 2021.12.22
10_리액트_input상태,useRef  (0) 2021.12.22