React Context + State = Constate 에 대하여

constate 라이브러리 깃헙 레포 링크

현재 개발하는 프로젝트에서 constate 라이브러리를 사용하고 있는데, 사용하면서 그냥 Context API 쓰는거랑 뭐가 다른지 크게.. 와닿지 않아서 이 기회에 공부해보고 싶고, 기타 프로젝트들에 어떻게 사용해야 constate를 더 잘 활용할 수 있을 지 알아보기 위해 constate를 탐구해보자.

Context API의 (재)등장과 그 한계

리액트 16.3 이 릴리즈 되면서, 기존에 존재하던 Context API 가 새로워졌다. 이미 많은 프로젝트에서 Context API 가 사용되고 있으니, Context API 사용법이나 개념에 대한 설명은 생략한다.

아래는 Context API 를 적용한 매우 간단한 코드 예시다.

const RandomPickerContext = createContext(null);

const RandomPickerProvider = ({ children }) => {
  const [result, setResult] = useState(null);
  return (
    <RandomPickerContext.Provider value={{ result, setResult }}>
      {children}
    </RandomPickerContext.Provider>
  )
}

const useRandomPicker = () => {
  return useContext(RandomPickerContext);
}

const Result = () => {
  const { result } = useRandomPicker();
  return (
    <div>
      {result ? 
        <span>{result} 당첨!</span>
        :
        <span>추첨 결과가 없습니다. 버튼을 눌러 추첨을 하세요.</span>
      }
    </div>
  );
}

const Control = () => {
  const { setResult } = useRandomPicker();
  
  const members = ['jenny', 'dave', 'jayjay', 'joseph', 'luis'];

  const onClick = () => {
    var item = members[Math.floor(Math.random() * members.length)];
    setResult(item);
  }

  console.log('Control component rendered');
  return (
    <button onClick={onClick}>추첨하기</button>
  );
}

const ContextApiBasic = () => {
  return (
    <RandomPickerProvider>
      <Result />
      <Control />
    </RandomPickerProvider>
  );
}

export default ContextApiBasic;

RandomPickerProvider 에서 result 값과 그 result를 update하는 로직을 모두 전달하고 있다.

Context API에서 이런식으로 코드를 짜는 것은 아주 일반적이고 자연스럽지만, 문제(개선 가능성..?)가 있다.

  • 어떤 문제일까?

    역시 성능 이슈다 !!!

    리덕스와 Context API의 주요 차이가 성능 면에서 나타나는 것은 많이들 알고 있을 것이다.

    리덕스에서는 컴포넌트에서 글로벌 상태의 특정 값을 의존하게 될 때 해당 값이 바뀔 때에만 리렌더링이 되도록 최적화가 되어있다. 그래서, 글로벌 상태 중 의존하지 않는 값이 바뀌게 될 때에는 컴포넌트에서 낭비 렌더링이 발생하지 않는다. 하지만 Context에는 이러한 성능 최적화가 이뤄지지 않는다. 컴포넌트에서 만약 Context의 특정 값을 의존하는 경우, 해당 값 말고 다른 값이 변경 될 때에도 컴포넌트에서는 리렌더링이 발생하게 됩니다.

    위 예시에서는 Control 컴포넌트의 버튼을 눌러서 result 값이 업데이트가 되면, Control 도 함께 리렌더링이 되고 있다. 예시처럼 간단한 컴포넌트에서는 별 문제가 안되겠지만, 상태도 다양해지고, 뷰도 다양해지게 되면 위와 같은 구조로 Context를 사용하게 되면 낭비되는 렌더링이 너무 많이 발생하게 되어 성능적으로 좋지 못하다.

따라서, Context API를 사용할 때, Context들을 적절히 쪼개주는 작업이 필요하다. 위 예시에서 Context를 쪼개줘봤다.

const ResultContext = createContext(null);
const ResultUpdateContext = createContext(null);

const RandomPickerProvider = ({ children }) => {
  const [result, setResult] = useState(null);
  return (
    <ResultContext.Provider value={result}>
      <ResultUpdateContext.Provider value={setResult}>
        {children}
      </ResultUpdateContext.Provider>
    </ResultContext.Provider>
  );
}

const Result = () => {
  const result = useContext(ResultContext);
  // ...이하 동일
}

const Control = () => {
	const setResult = useContext(ResultUpdateContext);
  // ...이하 동일
}

위 코드와 같이 ResultUpdateContext 를 만들어서 상태를 위한 Context와 상태 업데이트를 위한 Context를 따로 사용하게 된다면 성능적인 부분이 많이 해소가 된다. 이제는 버튼을 눌러도 Control 컴포넌트는 리렌더링을 하지 않게 된다.

Constate

Context를 사용하고 싶은데, 다루어야 하는 상태가 많아진 상황에서 성능도 챙기고, 개발도 편리하게 하고 싶다면, Constate가 좋은 방법(이라고 한다)

Constate는 Context를 기반으로 작동하는 라이브러리다. 상태를 위한 context 하나, 상태 업데이트를 위한 context 하나 이렇게 두 가지를 따로 만들었던 작업을 constate를 활용하면 하나의 함수로 간편하게 처리 할 수 있다.

  1. constate 설치
yarn add constate
  1. 위 RandomPicker 에 constate 적용
import { useState } from "react";
import constate from 'constate';

const useRandomPicker= () => {
  const [result, setResult] = useState(null);
  return { result, setResult };
}

const [RandomPickerProvider, useResultValue, useResultUpdate] = **constate**(
  useRandomPicker,
  value => value.result, // becomes useResultValue 
  value => value.setResult // becomes useResultUpdate
);

const Result = () => {
  const result = useResultValue();
  // ...이하 동일
}

const Control = () => {
  const setResult = useResultUpdate();
  // ...이하 동일
}

요약하자면 다음과 같이 표현할 수 있고,

const [Provider, ...hooks] = constate(useValue[, ...selectors]);

대충 내가 이해한 constate 사용법은 이렇다.

// Custom Hook 선언 
const useValue = () => {
	// 다양한 value, handlers 선언... 
  ....
	.... 

	return { ... } // 위에서 선언한 context로 넘겨주고 싶은 것들
}

const [Provider, useCustomHook1, useCustomHook2] = constate(
	useValue,
	selector1, // 이 아이는, useCustomHook1()의 결괏값
	selector2  // 이 아이는, useCustomHook2()의 결괏값
);

// 이런식으로 다른 component에서 사용한다
const Component1 = () => {
	const selector1 = useCustomHook1();  
}

const Component2 = () => {
	const selector2 = useCustomHook2();  
}

결론

알고 보니, 나는 constate를 제대로 활용하고 있지 못하고, selector의 분리 없이 (=context들을 분리 하지 않고) 사용하고 있어서 constate의 이점을 활용하고 있지 못했던 것이다. 앞으로 프로젝트에서 성능 개선과 함께 이런 부분들에 대해서 낭비되는 렌더링들을 잡는 작업을 할 예정이다!

사내 스터디 원본 자료 링크 - Copyrights to Jaeeun Lee

좋은 웹페이지 즐겨찾기