"지윤님 국내/해외를 토글 할 수 있는 스위칭 버튼을 달아주세요"
하나의 서비스의 처음부터 끝까지 내가 다 담당할 수 있다면 좋겠지만, 하나의 프로젝트를 다수의 개발자들과 함께 하기도 하고, 조직 이동을 통해 내가 담당하던 서비스를 다른 개발자에게 넘기거나 혹은 다른 개발자의 서비스를 내가 받아 담당하게 될 때도 있다. 그렇게 다른 사람이 짜 놓은(잘 돌아가는 코드 위에) 내 코드를 얹어야 할 때가 있다.
이번에는 그런 경험을 했다. 서비스가 확장되어 하나의 화면에서 토글을 통해 여러 데이터를 스위칭해서 보여줄 수 있게 코드를 얹어야 했다. 더 깔끔하게 분기칠 수 있는 방법은 없을까? 간략한 코드 예시를 통해서 코드에서 예상할 수 있는 문제점과 더 깔끔한 코드로 개선해 볼 수 있는 방법은 무엇일지-`하나의 컴포넌트 두개의 쿼리` 편을 공유해보고자 한다.
❪ 목차 ❫
❍ 구현해야 할 것
기존 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 |
---|