본문 바로가기
FE

좋은 코드란 무엇일까?(2) - 하나의 컴포넌트 두개의 쿼리

by Jiyoon-park 2023. 4. 23.

"지윤님 국내/해외를 토글 할 수 있는 스위칭 버튼을 달아주세요"

 

 

하나의 서비스의 처음부터 끝까지 내가 다 담당할 수 있다면 좋겠지만, 하나의 프로젝트를 다수의 개발자들과 함께 하기도 하고, 조직 이동을 통해 내가 담당하던 서비스를 다른 개발자에게 넘기거나 혹은 다른 개발자의 서비스를 내가 받아 담당하게 될 때도 있다. 그렇게 다른 사람이 짜 놓은(잘 돌아가는 코드 위에) 내 코드를 얹어야 할 때가 있다. 

 

이번에는 그런 경험을 했다. 서비스가 확장되어 하나의 화면에서 토글을 통해 여러 데이터를 스위칭해서 보여줄 수 있게 코드를 얹어야 했다. 더 깔끔하게 분기칠 수 있는 방법은 없을까? 간략한 코드 예시를 통해서 코드에서 예상할 수 있는 문제점과 더 깔끔한 코드로 개선해 볼 수 있는 방법은 무엇일지-`하나의 컴포넌트 두개의 쿼리` 편을 공유해보고자 한다.

 

 


 구현해야 할 것

기존 UI는 국내 데이터를 품고 있다. UI에 국내와 해외를 스위칭하는 버튼을 달면서, 스위칭이 될 경우,

 

  • 몇몇의 UI는 숨김 처리가 되어야하고,
  • UI에 적용되는 데이터가 국내 데이터 <-> 해외 데이터로 자유롭게 변경되어야 한다.

결과적으로 분기처리 할 곳은 두 곳이다 - UI와 data

 

 

 처음 구현한 것

기존 (국내 데이터를 들고 있는) 컴포넌트 구조는 아래와 같다.

// GroupComponent.tsx
function GroupComponent () {
  return (
    <Stack>
      <ItemComponent type="A"/>
      <ItemComponent type="B"/>
      <ItemComponent type="C"/>
    </Stack>
  )
}
// ItemComponent.tsx
function ItemComponent ({ type }: Props) {
  const { data } = useSomething();
  return (
    <Stack>
      <span>{data[type].title}</span>
      <span>{data[type].description}</span>
    <Stack>
  )  
}
​
function useSomething () {
  const {data: items} = useSuspensedQuery([`keyA`, param1, param2], () => api.get<ResponseA>(`urlA`))
  
  const result = item.reduce((acc, curr) => {
    // items를 원하는 데이터 구조로 가공
    ...
  }, {})
  
  return {
    data: result
  }
}

 

해외로 토글이 옮겨갔을 경우, 컴포넌트 변경점은

 

  • A 타입의 ItemComponent 는 숨김 처리가 되어야 한다.
  • 데이터가 해외 데이터로 변경되어야한다. 
// GroupComponent.tsx
​
// 변경점1. A 타입의 ItemComponent 는 숨김 처리가 되어야한다. 
function GroupComponent () {
  const { isOveresas } = useContext()
  return (
    <Stack>
      {isOveresas === true ? <ItemComponent type="A"/> : null}
      <ItemComponent type="B"/>
      <ItemComponent type="C"/>
    </Stack>
  )
}

 

컴포넌트를 그대로 사용하고 데이터를 불러오는 쿼리만 국내 데이터에서 해외 데이터로 쇽 바뀌었으면 좋겠는 마음에,

 

  • 기존의 국내 데이터를 불러오던 훅을 국내 데이터 hook으로 변경하고,
  • 해외 데이터를 불러오는 hook을 하나 추가로 만들었다.
  • 그리고 두개의 쿼리를 감싸는 hook을 만들어 flag로 data를 분기 쳐서 반환했다.
// ItemComponent.tsx
​
// 변경점2. 데이터가 해외 데이터로 변경되어야한다 
function ItemComponent ({ type }: Props) {
  const { data } = useSomething();
  return (
    <Stack>
      <span>{data[type].title}</span>
      <span>{data[type].description}</span>
    <Stack>
  )  
}
​
function useSomething () {
  const { isOveresas } = useContext()
  
  const domesticSomethingItems = useDomesticSomethingItems();
  const overseasSomethingItems = useOverseasSomethingItems();
​
  return isOverseas ? overseasSomethingItems : domesticSomethingItems;
}
​
function useDomesticSomethingItems () {
  const { isOveresas } = useContext()
  const {data: items} = useSuspensedQuery([`keyA`, param1, param2], () => api.get<ResponseA>(`urlA`), { enabled: isOverseas === false })
  
  const result = item.reduce((acc, curr) => {
    // items를 원하는 데이터 구조로 가공
    ...
  }, {})
  
  return {
    data: result
  }
}
                             
function useOverseasSomethingItems () {
  const { isOveresas } = useContext()
  const {data: items} = useSuspensedQuery([`keyB`, param1, param2, param3, param4], () => api.get<ResponseB>(`urlB`), { enabled: isOverseas === true })
    
  // 국내와는 또 다른 데이터 가공 로직
  ...
}

 

여기서 중요한 처리가 빠졌다. 기존 query에서는 suspense 옵션을 true로 주어, data가 undefined가 아니라 항상 존재한다고 보장받을 수 있었지만, enabled를 query의 조건으로 주게 되면서, enabled의 조건이 false인 경우, query는 불러와지지 않아 undefined를 반환한다. 그리고 undefined의 상태에서 가공 로직을 타게 되면, undefined에 메서드를 사용하여 에러가 발생한다.

data에 initial value를 주었다. 각 쿼리의 interface가 달라 가공 로직 또한 다르므로, 각 interface에 맞는 initial value를 주었다. 

function useDomesticSomethingItems () {
  const { isOveresas } = useContext()
  const { data: items = [] } = useSuspensedQuery([`keyA`, param1, param2], () => api.get<ResponseA>(`urlA`), { enabled: isOverseas === false })
  
  const result = item.reduce((acc, curr) => {
    // items를 원하는 데이터 구조로 가공
    ...
  }, {})
  
  return {
    data: result
  }
}
                             
function useOverseasSomethingItems () {
  const { isOveresas } = useContext()
  const { data: items = {} } = useSuspensedQuery([`keyB`, param1, param2, param3, param4], () => api.get<ResponseB>(`urlB`), { enabled: isOverseas === true })
    
  // 국내와는 또 다른 데이터 가공 로직
  ...
}

코드에서 예상가능한 문제점 및 생각해 볼 포인트

문제 1. 분기 처리가 각각의 컴포넌트에 일어나서, 추후 분기로 인한 차이점을 파악할 때 두 컴포넌트를 모두 신경 써야만 한다.

 

문제 2. 서로 다른 성격의 query 두 개를 하나의 hook으로 묶어 반환하려 하는 것이 맞을까?

 서로 다른 성격이란 무엇일까?

    ❟ query 요청 서버가 다르다

    ❟ request param이 다르다

    ❟ response interface가 다르다

 

문제 3. enabled로 조건 호출을 하기에 query에 걸어둔 suspense가 무용지물이 되어버렸다

    ❟ data를 불러온 이후 가공 로직에서 터지지 않게 하기 위해 initial value를 설정해주어야 한다.

    ❟ 심지어 api interface의 차이로 initial value도 형태가 다르다.

 

문제 4. 현재는 국내, 해외 두 타입이지만 만약에 또 다른 제3의 타입이 하나 더 생긴다면?

    ❟ 또 다른 boolean flag를 사용한다

    ❟ 기존의 flag에 변경을 준다?

 

그렇다면 해결방법은?

해결방법 1. 국내/해외에 따라 보여주는 타입을 상수화 해서 분기의 차이를 가시화하자.

// GroupComponent.tsx
const 사용할_타입 = {
  [Region.국내]: ['A', 'B', 'C']
  [Region.해외]: ['B', 'C'],
} as Record<Region, 타입[]>;
​
function GroupComponent () {
  const { region } = useContext();
  
  return (
    <Stack>
      {사용할_타입[region].map(x => (
        <ItemComponent type={x}/>
      ))}
    </Stack>
  )
}

 

해결방법 2. UI와 데이터를 각각의 컴포넌트에서 분기칠 것이 아니라, 하나의 컴포넌트에서 처리하자.

// GroupComponent.tsx
const 사용할_타입 = {
  [Region.국내]: ['A', 'B', 'C']
  [Region.해외]: ['B', 'C'],
} as Record<Region, 타입[]>;
​
function GroupComponent () {
  const { region } = useContext();
  const { data } = useSomthing();
  
  return (
    <Stack>
      {사용할_타입[region].map(x => (
        <ItemComponent data={data[x]}/>
      ))}
    </Stack>
  )
}
// ItemComponent.tsx - data와 엮여있지 않은 UI 컴포넌트로 만들기
function ItemComponent ({ data }: Props) {
  return (
    <Stack>
      <span>{data.title}</span>
      <span>{data.description}</span>
    <Stack>
  )  
}

 

해결방법 3. 각각의 데이터를 들고 있는 컴포넌트를 만들고, 컴포넌트를 분기치자!

enabled로 query에 조건을 달 필요도, boolean flag로 쿼리 호출에 분기를 칠 필요도 없어진다.

// GroupComponent.tsx
function GroupComponent () {
  const { region } = useContext();
  const { data } = useSomthing();
  
  return {
    region === Region.국내 ? <GroupCompoenent.Domestic /> : <GroupCompoenent.Overseas />
  }
}
​
GroupCompoenent.Domestic = function DomesticComponent () {
  const { data } = useDomesticSomthing();  
  return (
    <Stack>
      {사용할_타입[Region.국내].map(x => (
        <ItemComponent data={data[x]}/>
      ))}
    </Stack>
  )
}
​
GroupCompoenent.Overseas = function OverseasComponent () {
  const { data } = useOverseasSomthing();
  return (
    <Stack>
      {사용할_타입[Region.해외].map(x => (
        <ItemComponent data={data[x]}/>
      ))}
    </Stack>
  ) 
}

정리

  • 분기 처리가 되는 곳은 less is more
  • 유연한 변경과 확장을 위해 no more boolean props!
  • react-query에 enabled 조건을 넘겨줄 때에는 suspense를 걸었음에도 undefined로 넘어 올 수 있다.

 

 

'FE' 카테고리의 다른 글

좋은 코드란 무엇일까?(1) - 페이지네이션의 버튼들  (0) 2023.04.05