React Context를 사용하면서 겪은 문제

jkpapa·2023년 9월 26일
0

팀프로젝트

목록 보기
4/5

이번 글에서는 React 애플리케이션에서 상태 관리를 위해 Context API를 도입하면서 겪은 렌더링 문제와 그 문제를 어떻게 해결했는지에 대해 이야기하려고 합니다. Context API는 React에서 상태를 효과적으로 관리하는 도구 중 하나로, 우리 프로젝트에서 전역 상태를 관리하기 위해 도입하게 되었습니다.

Context란?

리액트 공식문서 Context 설명에 따르면

일반적으로 props를 통해 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달합니다. 하지만 props를 전달하는 과정에서 중간에 많은 컴포넌트를 거쳐야 하거나 애플리케이션에 있는 많은 컴포넌트가 동일한 정보를 필요로 하는 경우에는 번거롭고 불편해질 수 있습니다. Context를 통해 부모 컴포넌트는 정보를 props을 명시적으로 전달하지 않고도 그 아래의 트리에 있는 모든 컴포넌트가 얼마나 깊은지에 관계없이 사용할 수 있습니다.

Context는 props drilling보다 간편하게 컴포넌트간에 데이터를 전달할 수 있는 방법으로 생각할 수 있습니다.

이미지 출처: 리액트 공식문서

Context API 사용법

Context는 createContext함수를 이용해 Context객체를 만들 수 있습니다.

import { createContext } from 'react';
const SampleContext = createContext();

Context객체에는 Provider, Consumer 컴포넌트가 포함되어 있습니다.

  • Provider: Context를 구독하는 컴포넌트들에게 Context의 변화를 알리는 컴포넌트
  • Consumer: Context 변화를 구독해 변화시 재 렌더링하는 컴포넌트
import { createContext, useContext } from 'react';
const SampleContext = createContext();

function App() {
  return (
    <SampleContext.Provider value="Hello">
      <GrandParent />
    </SampleContext.Provider>
  );
}

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

function Parent() {
  return <Child />;
}

function Child() {
   const value = useContext(SampleContext);

  return <div>{value}</div>;
}

export default App;

이렇게 만들어 주면 props drilling 없이 원하는 값을 useContext 훅으로 가져올 수 있습니다. 만약 Context에 저장된 값이 변경되면 Context구독하는 컴포넌트는 다시 렌더링 됩니다.

Context와 렌더링 문제

프로젝트에서 Context API를 도입한 후, 다음과 같은 렌더링 문제를 겪게 되었습니다.

현재 보여지는 화면에서 카테고리를 변경했을 때, 이전 카테고리의 데이터로 렌더링이 이뤄진 다음에야 변경한 카테고리의 데이터로 렌더링되었습니다. 눈에 보이는 렌더링 문제라서 사용자 경험에 부정적인 영향을 미칠 것이라 생각해 해결해야 할 문제였습니다.

코드를 살펴보니 Consumer 컴포넌트에 Context의 상태를 의존값으로 하는 useEffect가 상태를 업데이트하고 있던 것이 원인이었습니다.

import { createContext, useContext, useState } from 'react';
// import CategoryContext

function MainContainer() {
  const [renderData, setRenderData] = useState(null);
  const [activeId, setActiveId] = useState(0);
  const { category } = useContext(CategoryContext);

  useEffect(() => {
    //... code
    
    setRenderData(newRenderData);
    setActiveId(newActiveId)
  },[category]);
  
  return (
      <div>
    	<MainContent 
    	  renderData = {renderData}
		  activeId = {activeId}
	      onCardClick = {setActiveId}
		/>
      </div>
  );
}
  • renderData: category별 카드 컴포넌트에 렌더링 될 데이터
  • activeId: 선택된 카드 컴포넌트의 Id값

이런 방식으로 구현하면 CategoryContextcategory 값이 변경되었을 때, useEffect에 설정한 상태 업데이트 로직은 렌더링 이후에 실행됩니다.

따라서 이전 카테고리의 데이터가 잠시 보였던 것입니다.

문제 해결

처음 문제를 해결하려고 했을 때는 렌더링 문제니까 비교작업으로 하위 컴포넌트의 렌더링을 막아버리면 어떻겠나 생각했습니다.
하지만 컴포넌트가 두 번 호출되는 것은 변하지 않으며, 불필요한 비교작업이 추가되는 것이었습니다. 이 방법은 근본적인 해결방법이 아니라고 생각했습니다.

renderDataactiveIdcategory에 영향을 받습니다. 근본적인 원인은 서로 연관되어 있는 상태들이 분리되어 있고 useEffect로 불필요한 상태 업데이트 작업을 진행한 점이었습니다.

따라서 저는 useEffect를 제거하고 렌더링시에 화면에 보여질 데이터를 category값에 따라 연산 후 하위 컴포넌트로 전달하는 방식을 선택했습니다.

import { createContext, useContext, useState } from 'react';
// import CategoryContext

function MainContainer() {
  let nextRenderData, nextActiveId;
  const [activeId, setActiveId] = useState(0);
  const { category } = useContext(CategoryContext);
  
  nextRenderData = getRenderData(category);
  nextActiveId = getActiveId(category, activeId);
  
  return (
      <div>
    	<MainContent 
    	  renderData = {nextRenderData}
		  activeId = {nextActiveId}
	      onCardClick = {setActiveId}
		/>
      </div>
  );
}

이런식으로 작성하면 별도의 상태 업데이트 없이 category, activeId가 변경되었을 때, 컴포넌트 내부에서 데이터를 연산하여 전달하기 때문에 한 번의 렌더링만 일어나게 됩니다.

느낀점

리액트에서 상태관리는 어렵지만 매우 중요하다.

리액트의 핵심 특성 중 하나는 가상 DOM을 사용하여 뷰 업데이트를 변경사항만으로 효율적으로 처리하는 것입니다. 상태관리를 소홀히하면 불필요한 렌더링 발생시켜, 리액트의 장점을 잃어버리기 때문에 사용할 이유가 없어진다고 생각했습니다.
또한 의도한 데이터가 화면에 보여지지 않고 상태에 따라서 엉뚱한 데이터가 화면에 보여질 수 있기때문에 상태관리는 정말 중요하다고 할 수 있습니다.

useEffect에서 가급적 상태를 업데이트하지 말자

이번 문제는 useEffect사용이 간편하고, 기능이 많다보니 깊은 고민을 하지 않고 사용해서 발생한 문제이기도 합니다. useEffect는 렌더링 이후에 실행이 되기 때문에 정말 필요한 경우가 아니라면 상태를 업데이트하는 작업은 지양해야 할 것입니다.

마무리

프로젝트를 진행하면서 다른 문제도 많았습니다. 그러나 대부분이 이 글에서 다룬 문제로부터 파생되었던 오류들이었습니다. 상태관리를 소홀히하면 예상하지 못한 곳에서 문제가 많이 발생할 수 있으며, 오류를 추적하기에도 어렵습니다.

아마도 근본적인 진짜 원인은 저에게 있다고 생각합니다. 그저 기능구현에만 급급해서 화면에 데이터가 잘 보여지는 것으로 만족했습니다. 역시 사람은 맞아봐야 아는 걸까요 ㅋㅋ.. 개발을 잘하는 사람은 기능 구현을 많이, 빨리"만"하는 사람이 아니라는 것을 깨달았습니다. 구현을 빨리 하려다보니 오히려 오류를 추적하고 개선하는데 오래걸렸거든요.

다음부터는 코딩, 개발 이전에 설계와 백로그 같은 비개발적인 영역에 더욱 시간을 쏟아야 할 것 같습니다.

참고자료:
https://react.dev/learn/passing-data-deeply-with-context
https://velog.io/@jay/you-might-need-useEffect-diet

0개의 댓글