smob_logo
smob_logo
header-image
REACTPOS 통합 조회 페이지 리팩토링Context API를 의존성 분리하기2024.04.16

POS 시스템에서 다양한 유형의 데이터를 조회할 때, 검색 타입에 따라 다른 컴포넌트를 렌더링하고 있었습니다. 그러나 이 방식은 코드의 복잡도를 높이고 유지보수를 어렵게 만들었습니다. 특히 새로운 검색 타입이 추가될 때마다 코드를 수정해야 했고, 컴포넌트 간의 의존성이 높아지는 문제가 발생했습니다.

기존 코드 구조

검색 타입에 따라 해당 컴포넌트를 직접 호출하고, 각 컴포넌트에서 검색 쿼리 결과를 처리하고 있었습니다.

if (검색타입 === '티켓') return <티켓 />;
else if (검색타입 === '패키지') return <패키지 />;
else if (검색타입 === '선발행권') return <선발행권 />;
...

각 컴포넌트 내부에서는 조건부 렌더링을 통해 다른 컴포넌트를 호출하는 방식이 사용되었습니다.

// 티켓 컴포넌트
const 티켓 = () => {
  const { 티켓 } = useQuery(TICKET_QUERY);
  ...
  return (
    <div>
      <span>{티켓 제목}</span>
      <span>{티켓 가격}</span>
      ...

    </div>
    // 검색 결과에 따라 다른 컴포넌트 렌더링
    { 패키지 조건 && <패키지 /> }
    { 시즌패스 조건 && <시즌패스 /> }
    { 선발행권 조건 && <선발행권 /> }
    { 비즈니스파트너몰 조건 && <비즈니스파트너몰 /> }
    { 제휴권 조건 && <제휴권 /> }
    { 법인회원권 조건 && <법인회원권 /> }
  );
};

또한, 각 컴포넌트에서 여러 개의 상태 관리 로직과 쿼리를 혼재하여 사용하고 있었습니다.

const 티켓 = () => {
  const [티켓State, set티켓State] = useState();
  const [패키지State, set패키지State] = useState();
  const [법인회원권State, set법인회원권State] = useState();

  const { data: 티켓Data } = useQuery(티켓_QUERY);
  const { data: 패키지Data } = useQuery(패키지_QUERY);
  const { data: 법인회원권Data } = useQuery(법인회원권_QUERY);

  useEffect(() => {
    // 상태 관리 로직
  }, [deps]);

  return (
    <div>
      <span>{티켓 이름}</span>
      <span>{티켓 가격}</span>
      ...
    </div>
  );
};

문제점

  1. 재귀 호출로 인한 무한 루프 발생 가능성: 컴포넌트 내에서 다른 컴포넌트를 조건부로 렌더링하는 방식은 재귀 호출로 인해 무한 루프가 발생할 수 있습니다.
  2. 검색 쿼리와 상태 관리 로직의 혼재: 여러 개의 쿼리와 상태 관리 로직이 한 컴포넌트에 섞여 있어 코드의 가독성과 유지보수성이 떨어집니다.
  3. 확장성 부족: 새로운 검색 타입이 추가될 때마다 기존 코드를 수정해야 하며, 이는 버그 발생의 원인이 될 수 있습니다.

Context API를 활용하여 개선하기

이러한 문제를 해결하기 위해 Context API를 활용하여 컴포넌트 간의 의존성을 분리하고, 검색 로직을 중앙에서 관리하도록 구조를 개선했습니다.

Step 1: 통합된 검색 결과 타입 정의

검색 쿼리 결과를 다루는 공통 인터페이스를 정의했습니다.

interface 검색Context타입 {
  티켓: 티켓컴포넌트타입 | null;
  선발행권: 선발행권컴포넌트타입 | null;
  법인회원권: 법인회원권컴포넌트타입 | null;
  BP: BP몰컴포넌트타입 | null;
  유저: 유저컴포넌트타입 | null;
  밴드: 밴드컴포넌트타입 | null;
  상품교환권: 상품교환권컴포넌트타입 | null;
  패키지: 패키지컴포넌트타입 | null;
  시즌패스: 시즌패스컴포넌트타입 | null;
  제휴권: 제휴권컴포넌트타입 | null;
}

Step 2: 검색 Context 생성

각 검색 타입마다 개별적인 Context를 생성했습니다.

const 티켓Context = createContext<검색Context타입 | undefined>(undefined);
const 상품패키지Context = createContext<검색Context타입 | undefined>(undefined);
const 밴드Context = createContext<검색Context타입 | undefined>(undefined);
const 유저Context = createContext<검색Context타입 | undefined>(undefined);
const 법인회원권Context = createContext<검색Context타입 | undefined>(undefined);
const 쿠폰Context = createContext<검색Context타입 | undefined>(undefined);
const 야놀자Context = createContext<검색Context타입 | undefined>(undefined);

Step 3: 각 검색 타입에 맞는 Provider 생성

검색 타입에 따라 적절한 Provider를 생성하고 데이터를 주입했습니다.

const 티켓ContextProvider = ({ children }) => {
  const { data } = useQuery(TICKET_QUERY);

  const 티켓 = 검색결과에_따른_비즈니스로직_함수(data);
  const 선발행권 = 검색결과에_따른_비즈니스로직_함수(data);
  // ... 다른 필드들

  const value: 검색Context타입 = {
    티켓,
    선발행권,
    법인회원권,
    BP,
    유저,
    밴드,
    상품교환권,
    패키지,
    시즌패스,
    제휴권,
  };

  return <티켓Context.Provider value={value}>{children}</티켓Context.Provider>;
};

// 다른 검색 타입도 동일하게 생성
const 상품패키지ContextProvider = ({ children }) => {
  // ... 유사한 로직
};

Step 4: 검색 결과 Provider 적용

검색 타입에 따라 적절한 Provider를 적용했습니다.

const 검색결과ContextProvider = ({ children }: { children: ReactNode }) => {
  const queryUid = useSearchUid();

  // 검색 타입에 따라 적절한 Provider를 매핑해주는 역할
  const providerMap: Record<EnumSearchResultType, ({ children }: { children: ReactNode }) => React.JSX.Element> = {
    밴드: 밴드ContextProvider,
    유저: 유저ContextProvider,
    티켓: 티켓ContextProvider,
    법인회원권: 법인회원권ContextProvider,
    상품패키지: 상품패키지ContextProvider,
    쿠폰: 쿠폰ContextProvider,
    야놀자: 야놀자ContextProvider,
  };

  const Provider = providerMap[getSearchResultType(queryUid)];

  return <Provider>{children}</Provider>;
};

const 검색결과컴포넌트 = () => (
  return (
    <검색결과ContextProvider>
      <검색결과를그릴카드컴포넌트들 />
    </검색결과ContextProvider>
  )
);

Step 5: 검색 결과 사용

각각의 검색 카드 컴포넌트에서 Context를 통해 필요한 데이터를 가져와 사용했습니다.

const 티켓 = () => {
  const { 티켓 } = useContext(티켓Context);

  if (!티켓) return null;

  return (
    <div>
      <span>{티켓.이름}</span>
      <span>{티켓.가격}</span>
    </div>
  );
};

const 패키지 = () => {
  const { 패키지 } = useContext(패키지Context);

  if (!패키지) return null;

  return (
    <div>
      <span>{패키지.이름}</span>
      <span>{패키지.가격}</span>
    </div>
  );
};

개선된 점

1. 가독성 향상

각 컴포넌트는 자신의 역할에만 집중할 수 있어 코드의 가독성과 유지보수성이 크게 향상되었습니다. 복잡한 로직이 중앙에서 관리되므로, 컴포넌트 내부는 더 간결해졌습니다.

2. 유지보수성 강화

검색 로직이 중앙에서 관리되므로, 개별 컴포넌트를 수정할 때 다른 부분에 영향을 주지 않습니다. 예를 들어, 티켓 검색 로직에 변경이 필요할 때 다른 Context나 컴포넌트에 영향을 주지 않고 수정할 수 있습니다.

// 티켓 검색에 온라인 유저를 추가해야하는 예시
const 티켓ContextProvider = ({ children }) => {
  const { data } = useQuery(TICKET_QUERY);

  const 온라인유저 = data?.result?.ticket?.온라인유저정보필드;

  const value: 검색Context타입 = {
    ...기존값,
    유저: 온라인유저 || null,
  };

  return <티켓Context.Provider value={value}>{children}</티켓Context.Provider>;
};

3. 확장성 증가

새로운 검색 타입이 추가되더라도 기존 구조를 크게 변경할 필요 없이, 새로운 Context와 Provider를 추가하기만 하면 됩니다.

const 외부발행쿠폰Context = (createContext < 검색Context타입) | (undefined > undefined);

const 외부발행쿠폰ContextProvider = ({ children }) => {
  const { data } = useQuery(외부발행쿠폰_QUERY);

  const value: 검색Context타입 = {
    티켓: 검색결과에따라보여줄비즈니스로직(data),
    선발행권: 검색결과에따라보여줄비즈니스로직(data),
    // ... 다른 필드들
  };

  return <외부발행쿠폰Context.Provider value={value}>{children}</외부발행쿠폰Context.Provider>;
};

const providerMap = {
  밴드: 밴드ContextProvider,
  유저: 유저ContextProvider,
  티켓: 티켓ContextProvider,
  법인회원권: 법인회원권ContextProvider,
  상품패키지: 상품패키지ContextProvider,
  쿠폰: 쿠폰ContextProvider,
  야놀자: 야놀자ContextProvider,
  외부발행쿠폰: 외부발행쿠폰ContextProvider,
};

4. 테스트 용이성

Context API를 활용함으로써 외부에서 필요한 데이터를 주입할 수 있는 유연성이 증가했습니다. 이는 테스트나 설정 변경 시에 매우 유용합니다. 예를 들어, Context Provider를 사용할 때 외부에서 필요한 의존성을 주입함으로써 컴포넌트의 재사용성과 테스트 용이성이 향상됩니다.

// 외부에서 주입 가능한 Provider 예시
const 티켓ContextProvider = ({ children, initialData }) => {
  const { data } = useQuery(TICKET_QUERY);

  const 티켓 = initialData?.티켓 || 티켓비즈니스로직(data);
  // ... 다른 필드 처리

  const value: 검색Context타입 = {
    티켓,
    // ... 나머지 필드
  };

  return <티켓Context.Provider value={value}>{children}</티켓Context.Provider>;
};

// 사용 시에 외부에서 데이터 주입 가능
<티켓ContextProvider initialData={{ 티켓: mockTicketData }}>
  <티켓컴포넌트 />
</티켓ContextProvider>;
#react
#react-context
#refactoring
#frontend
avatar
By. 정규재안녕하세요. 잘 부탁 드립니다.