인프런 커뮤니티 질문&답변

blossom_mind님의 프로필 이미지
blossom_mind

작성한 질문수

[2024] 한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지

11.1) Context란

context에 관해서 질문드립니다.

해결된 질문

작성

·

1K

3

DiaryStateContext.provider 2중으로 넣는 부분 질문드립니다 .

<DiaryStateContext.provider value={data} > 만 써야 되는 이유로 onCreate,onEdit,onRemove 등은 data의 상태가 변경될떄(c,u,d) 마다 리렌더링이 생겨서 최적화가 풀린다고 하셨는데

  1. 상태가 변경된다는 말이 onCreate 할떄 data에 실제 생성 ,변경, 삭제 되는 그 로직으로 인한 배열데이터가 변경된다는 말인건가요 ?

 

  1. 그리고. context가 App.js로 부터 data만 받을떄 어차피 onCreate,onEdit,onRemove 함수호출당시에도 data가 변경되고 setData로 변경이 된후에는 어쩃든 data가 추가든 삭제든 수정이든 변경이 될텐데 그러면 app 컴포넌트에서 바뀐 data로 DiaryStateContext 로 데이터를 다시 내려 줄꺼고 그러면 그 하위에 있던 컴포넌트 들은 다시 리렌더링 되지 않을까요 ?

 

 

답변 1

2

이정환 Winterlood님의 프로필 이미지
이정환 Winterlood
지식공유자

안녕하세요 이정환입니다.

1번

상태가 변경된다는 말의 의미는 App 컴포넌트의 data State의 변경을 의미합니다. 말씀하신대로 onCreate, onUpdate, onDelete가 실행되면 일기 배열 상태인 data State를 변경하기 때문에 상태가 변경됩니다.

2번

네 맞습니다. DiaryStateContext.Provider에 전달하는 Props가 변경되면 그 아래의 컴포넌트들은 리렌더가 발생합니다.

그렇기 때문에 불 필요한 리렌더가 발생하지 않도록 React.memo 등을 이용해 컴포넌트들을 메모이제이션해야 합니다. 그런데 data와 onCreate, onUpdate, onDelete 함수를 하나의 객체로 묶어 하나의 Context를 통해 공급하면 결국 data의 값이 바뀔 때 마다 이 객체 자체가 새롭게 생성됩니다.

반면 값과 업데이트 함수들을 두개의 Context로 분리(DiaryStateContext, DiaryDispatchContext)하면 값(data)과 상태변화 함수(onCreate, onUpdate, onDelete)를 각각 다른 객체로 묶어주기 때문에 값의 변화에도 상태변화 함수를 묶는 객체가 다시 생성되지는 않습니다(useMemo 적용시)

질문자님께서 무엇을 궁금해 하시는지 정확히 파악하지 못해서 뜬 구름 같은 답변이 되었을 수 있습니다(양해 부탁드립니다) 혹시 추가적으로 궁금하시거나 이해가 안되시는 부분이 있으면 추가 질문 부탁드립니다.

blossom_mind님의 프로필 이미지
blossom_mind
질문자

<DiaryStateContext.Provider value={store}>

<DiaryDispatchContext.Provider value={memoizedDispatch}>

........

</DiaryDispatchContext.Provider>

</DiaryStateContext.Provider>

  1. Props가 변경되면 그 아래의 컴포넌트들은 리렌더가 발생합니다.

 

  1. 반면 값과 업데이트 함수들을 두개의 Context로 분리(DiaryStateContext, DiaryDispatchContext)하면 값(data)과 상태변화 함수(onCreate, onUpdate, onDelete)를 각각 다른 객체로 묶어주기 때문에 값의 변화에도 상태변화 함수를 묶는 객체가 다시 생성되지는 않습니다(useMemo 적용시) 라고 하셨는데 .

궁금한게 1번 문장으로 생각하면

data <= (위 코드에선 store)..
store가 변경되면 컴포넌트 리렌더가 발생하고

그 아래 속해있는 컴포넌트인
<DiaryDispatchContext.Provider value={memoizedDispatch}>
부분이 하위 컴포넌트라 리렌더링이 되지 않을까 라고 생각을 하고 있는데요 .

 

1, 근데 2번을 보면 따로 분리 하면 각각 다른 객체로 묶어주기 때문에 값의 변화에도 상태변화 함수를 묶는 객체가 다시 생성되지는 않습니다라고 해서 좀 이해가 되지 않습니다 .

 

  1. 그리고 prop 변경이 data 배열의 길이나 요소의 변경둘다 의미하는건지 ...렌더링이랑 객체 생성이랑 같은의미로 봐도 되는지가 궁금하네요..

이정환 Winterlood님의 프로필 이미지
이정환 Winterlood
지식공유자

안녕하세요 이정환입니다.

하나씩 천천히 정리해 보겠습니다!

우선 Context.Provider에 공급하는 value Props의 값이 변하면 Provider 하위의 컴포넌트들은 모두 리렌더 됩니다.

그런데 이 때 React.memo를 이용해 컴포넌트를 감싸 강화된(메모이제이션 된) 컴포넌트로 만들어주면 불 필요한 리렌더를 막을 수 있습니다. 따라서 React.memo로 감싸져 있는 컴포넌트들은 useContext로 구독하는 값이 변경되지 않는 이상 리렌더 되지 않습니다.

자 그러면 다음과 같은 코드가 있다고 가정하겠습니다.

// src/App.js

const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider
          value={{
            onIncrease,
            onDecrease
          }}
        >
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

App 컴포넌트는 1개의 State를 가지고 있습니다. 그리고 이 State를 변경시키는 함수 onIncrease, onDecrease도 가지고 있습니다.

상태와 상태 변경함수를 Context를 이용해 Props Drilling 없이 컴포넌트 트리 전체에 공급해주기 위해 2개의 Context를 만들었습니다. 하나는 State를 공급할 StateContext, 하나는 상태변화 함수들을 공급할 DispatchContext입니다.

각각의 Context.Provider 컴포넌트에 value Props를 설정해 컴포넌트 트리에 값을 공급합니다. StateContext.Provider에는 state를 DispatchContext.Provider에게는 {onIncrease, onDecrease} 객체를 전달합니다.

다음으로 2개의 자식 컴포넌트를 만듭니다. ChildA, ChildB입니다.

const ChildA = React.memo(function () {
  const state = useContext(StateContext);
  useEffect(() => {
    console.log("A");
  });
  return <div>{state}</div>;
});

const ChildB = React.memo(function () {
  const { onIncrease, onDecrease } = useContext(DispatchContext);
  useEffect(() => {
    console.log("B");
  });
  return (
    <div>
      <button onClick={onDecrease}>-</button>
      <button onClick={onIncrease}>+</button>
    </div>
  );
});

ChildA는 State만 공급받아 화면에 렌더링하는 컴포넌트입니다.

반면 ChildB는 2개의 버튼이 있고 각 버튼을 클릭하면 DispatchContext로 부터 공급받은 상태변화 함수를(onIncrease, onDecrease) 호출하는 컴포넌트입니다.

정리하면 ChildA -> StateContext / ChildB -> DispatchContext의 값을 공급받습니다(구독이라고 표현해도 됩니다)

이 때 ChildA와 ChildB 모두 React.memo를 적용합니다. 이를 통해 우리가 기대하는 건 state의 값이 바뀌면 ChildA 컴포넌트만 리렌더되고, onIncrease, onDecrease 함수가 재 생성되면 ChildB 컴포넌트만 리렌더 되기를 바랍니다.

정말 그렇게 될까요? 실제로 코드샌드박스에서 실험 해 보았습니다.

image

아쉽게도 ChildA, ChildB 두 컴포넌트가 다 리렌더됩니다. 왜 이런걸까요? 분명히 React.memo로 감싸 강화한 컴포넌트는 자신이 구독하는 컨텍스트가 공급하는 값이 바뀌지 않으면 리렌더 되지 않는다고 했는데요?

문제는 App 컴포넌트에 있습니다. App 컴포넌트의 코드를 다시 보겠습니다.

const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider
          value={{
            onIncrease,
            onDecrease
          }}
        >
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

DispatchContext의 value Props에 중점을 두고 살펴보겠습니다. 사실 이 객체는 App 컴포넌트의 state가 업데이트 되면 재 생성됩니다. App 컴포넌트가 리렌더되기 때문입니다. 컴포넌트는 함수이기 때문에 컴포넌트의 리렌더는 함수를 다시 호출하는 거라고 했습니다. 따라서 함수 내부에 선언한 객체도 다시 선언됩니다.

결국 state가 변경되면 DispatchContext.Provider에 공급하는 값이 바뀝니다. 그럼 React.memo를 ChildA, B에 적용한 이유가 사라졌습니다.

문제는 onIncrease, onDecrease를 감싸는 객체가 재 생성되서 발생합니다. 그럼 이 객체를 다시 생성하지 않도록 만들면 문제를 해결할 수 있습니다. 이럴 때 useMemo를 활용하면 됩니다.

const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  const memoizedDispatch = useMemo(() => ({ onIncrease, onDecrease }), []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider value={memoizedDispatch}>
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

변수 memoizedDispatch를 만들고 이 변수에 useMemo로 메모이제이션 한, 다시 생성되지 않도록 만든 onIncrease, onDecrease를 감싼 객체를 저장합니다. 이 memoizedDisaptch는 state의 값이 바뀌어도 재 생성되지 않습니다.

그 다음 memoizedDispatch를 DispatchContext.Proider의 value Props로 전달합니다. 이제 DispatchContext.Provider는 App 컴포넌트의 state가 바뀌어도 전달받는 Props가 달라지지 않습니다.

그럼 이제 버튼을 클릭해 state를 업데이트 해도 React.memo가 적용되고, StateContext를 구독하지 않는 ChildB 컴포넌트가 리렌더 되지 않는지 확인해 보겠습니다.

image

+버튼을 연타해도 ChildA 컴포넌트만 리렌더 되는것을 알 수 있습니다.

정리하자면 Context.Provider에 제공하는 value Props가 바뀌면 이 하위의 컴포넌트들은 모두 리렌더 된다.

이것을 막고 싶다면 리렌더를 방지하고 싶은 컴포넌트에 React.memo를 적용하면 된다.

그러나 함수, 객체 등을 value Props로 전달할 경우 Context에 데이터를 공급하는 컴포넌트가 리렌더 되면 함수나 객체도 다시 생성되기 때문에 리렌더 방지가 효과적으로 이루어지지 않는다.

이를 useMemo를 이용해 해결할 수 있다.

이렇게 정리해 보았습니다.

blossom_mind님의 프로필 이미지
blossom_mind

작성한 질문수

질문하기