현재 중고나라 웹 개발팀에서 주니어로 근무하며 새로운 서비스를 오픈하는 과정에서 고군분투하며 배운 것들을 정리한 글입니다.
편의점 픽업 서비스는 기존 중고거래 시 가능했던 직거래와 택배거래 이외에, 편의점을 통해 비대면으로 시간에 상관 없이 중고상품을 맡기고 찾을 수 있는 새로운 거래 서비스입니다.
세븐일레븐과 중고나라가 독점으로 제휴하여 진행하는 서비스이기도 하고, 중고거래 중계 플랫폼으로서 고객에게 더 나은 중고거래 방법을 제시하여 중고 거래를 활성화에 기여할 수 있는 기회이기 때문에 서비스를 잘 만들고 싶은 생각이 들었습니다.
웹 개발팀은 편의점 픽업을 위한 교환권 개발과 판/구매내역 및 결제 페이지 개편을 담당했습니다. 그 중 저는 편의점 교환권 개발을 담당하였습니다.
기획서 상에서 편의점 교환권은 형태가 다른 2가지의 컴포넌트가 필요하다는 걸 알았습니다. 즉 역할은 동일한 컴포넌트가 서로 다른 형태를 가진다는 점에서, 어떻게 하면 로직은 공유하면서 스타일에 유연한 컴포넌트를 만들 수 있을지 고민하게 되었습니다.
토스의 한재엽님의 SLASH22 강연을 통해, 변경에 유연한 컴포넌트를 위한 3가지 기준을 제시해 주신 것이 생각났습니다.
컴포넌트는 데이터를 주입받아 사용자와 상호작용을 할 수 있는 최소 UI 단위입니다. 즉, 컴포넌트의 구성 요소는 데이터, 상호작용, UI로 나뉠 수 있습니다.
그렇다면 컴포넌트 구성 요소 중 상대적으로 변경에 취약하지 않은 데이터와 취약한 UI를 분리하는 것만으로도 유연한 컴포넌트 설계가 가능합니다(강연에서 이를 Headless라고 소개하고 있습니다)
커스텀 훅(useVoucher)을 생성하여 각 컴포넌트가 데이터를 사용할 수 있도록 하였습니다.
// voucherPage
const { selectedVoucher, voucherList } = useVoucher();
// voucherPopup
const { selectedVoucher } = useVoucher();
즉, 두 컴포넌트가 하나의 데이터 훅에 의존하는 형태가 됩니다. 이렇게 설계하게 되면 훅에서 변경한 사항이 두 컴포넌트에 모두 전파된다는 장점이 있습니다.
주니어 때는 의존성이라는 단어가 무조건 부정적이라고만 생각했는데, 개발을 하면서 의존성이 나쁜게 아니라 의도하지 않은 의존성에 의한 "통제 불가능한" 부수 효과를 피해야 한다는 점을 알게 되었습니다.
이 경우에는 교환권에 핵심이 되는 데이터를 두 컴포넌트에 의도적으로 의존하게 두어, 데이터의 변경 사항이 안전하게 두 컴포넌트에 반영될 수 있습니다. 만약 API url이 변경되어, 동일한 API 데이터를 사용하는 컴포넌트를 수정해야 한다고 생각해보면, 하나의 훅으로 의존하지 않게 될 경우 모든 컴포넌트를 일일히 수정해야 하는 번거로움이 생기게 됩니다. 또 혹시 모르는 누락이 발생하여 의도치 않은 에러가 발생할 가능성이 있습니다.
교환권 컴포넌트를 구성하기 위해 필요한 데이터는 다음과 같습니다.
또 상황에 따라 여러 개의 교환권(교환권 리스트)을 보여줘야 하는 경우, 해당 컴포넌트를 슬라이드로 이동할 수 있어야 하고 현재 보여주는 교환권의 순서 역시 보여줘야 합니다.
이 때문에 단일 교환권 조회용 API와 교환권 리스트 조회용 API가 나뉘어 있어 상황에 따라 다른 API를 호출해야 합니다. 심지어 교환권 리스트의 경우 타입이 배열이기 때문에, 타입까지 신경써줘야 했습니다.
보통은 이런 상황에서 컴포넌트 내의 조건에 따라 if 문이 중첩되어 나중에 코드를 유지보수하거나 처음 보는 입장에서 이해하기 어려운 구조가 될 수 있습니다. 뿐만 아니라 API를 호출 완료 되기 전 loading 까지 보여줘야 하는 상황에서는 더욱 가독성이 좋지 않을 거 같았습니다.
예전 React를 공부하면서, 컴포넌트 패턴 중 컴파운드 컴포넌트 패턴을 사용한 것을 본 적이 있습니다. 컴포넌트를 정의할 때, 필요한 기능들을 응집하여 서로 상태와 로직을 공유하는 방법입니다. 이렇게 설계를 할 경우, 실제 컴포넌트를 사용하는 환경에서 보다 선언적인 방법으로 코드 구현이 가능해집니다.
무엇보다 전 이 방법의 장점이라고 생각이 들었던 건, 컴포넌트 내의 기능들이 어떤 역할을 하는지 한 눈에 알 수 있다는 장점이라고 생각합니다. 예를 들어 교환권 바코드 영역을 표시하기 위해 정의한 Voucher.Barcode 의 경우, 코드만 보아도 해당 영역이 교환권의 바코드를 보여준다는 걸 한 눈에 알 수 있다는 점이 좋았습니다.
물론 코드를 작성할 때, 단순히 가독성 한 가지만을 척도로 보아서는 안 됩니다. 하지만 제가 입사 당시에 손 봐야 했던 코드들이 컴포넌트 구분도 안 된 상태로 몇 백줄이나 되었기에, 적어도 내 손으로 만든 컴포넌트는 다른 사람이 보았을 때도 빠르게 이해될 수 있도록 하자는 마인드를 가지게 되었습니다. 저도 데어 보았기 때문이죠 😂
cosnt VoucherPage = () => {
const { selectedVoucher } = useVoucher()
...
return
<Voucher voucherData={selectedVoucher}>
<Voucher.Barcode />
...
</Voucher>
}
이렇게 가장 상위 Wrapper에 현재 선택된 교환권 정보를 담아 보내면, 해당 데이터를 Voucher 컴포넌트 내부에서 공유하기 때문에 별도로 바코드 컴포넌트에 데이터를 전달하지 않아도 됩니다. 확실히 사용하는 환경에서 신경써야 하는 부분이 적어지니 편합니다 :)
저는 합성 컴포넌트(=컴파운드 컴포넌트)를 구현하기 위해, 내부 데이터 및 로직 공유를 위해 Context API 를 사용했습니다. Context API의 단점으로 상태가 변경되었을 때, Provider로 감싼 하위 요소가 모두 리랜더링 된다는 점입니다.
하지만 애초에 교환권 정보를 랜더링하면, 그 다음으로 상태가 크게 변동될 상황이 없고 리랜더링 되어도 교환권 요소가 복잡하지 않아 랜더링 비용이 크지 않다고 생각했습니다(물론 실제 성능 측정을 하지 않아서, 이 부분은 숙제로 남겨두겠습니다)
컴포넌트를 설계하면서 또 피하고 싶었던 것은 데이터 loading 상태에 따른 분기처리였습니다.컴포넌트의 역할(교환권 컴포넌트라면, 교환권 데이터를 받아 UI를 보여주는 것)과 무관한 분기 처리는 프로그래머가 보다 핵심 로직 구현하는데 방해 요소가 된다고 생각합니다. 물론 가독성도 좋지 않습니다.
React 18에서는 이런 Data Fetching 을 위한 Suspense 기능을 지원하고 있습니다(이전에는 React.Lazy를 통해 Lazy loading 컴포넌트를 기다리기 위해 사용되는 용도지만, 이를 Data fetching으로 확장)
하지만 현재 저희 팀에서는 React 17 버전을 사용하고 있기 때문에, 해당 기능을 사용할 수 없었습니다. 어떻게 실제 교환권 컴포넌트를 사용하는 환경에서 이런 분기 처리를 신경쓰지 않도록 할 수 없을지 고민하다, 결국 훅에서 반환하는 교환권 정보에 교환권 상태를 추가하고, 이를 전달받은 합성 컴포넌트 내부에서 상태에 따른 분기처리를 하도록 했습니다.
const useVoucher = () => {
...
return {
selectedVoucher : {
...
voucherStatus : ... // 'Loading', 'Failed', 'Success'
}
}
const Voucher = ({ voucherData }) => {
const { voucherStatus } = voucherData
// type guard
if(!isSuccessStatus(voucherStatus)){
const isLoading = voucherStatus === 'Loading'
return
isLoading ? <Loading /> : <Error />
}
...
}
react query는 data-fetching 과정에서 로딩, 에러, 성공 상태를 제공하기 때문에, 해당 상태에 따라 voucherStatus를 업데이트하여, Voucher 컴포넌트 내부에 전달하여 로딩과 에러 상태를 감추었습니다.
물론 이 방법이 선언적인 방식은 아니라고 생각합니다. 오히려 분기 처리를 감추었다고 표현하는 것이 맞을 것 같습니다. 하지만, 개발자는 단지 훅을 통해 데이터를 전달받고 Voucher 컴포넌트의 props로 주입하면 되기 때문에 위에서 말한 분기처리로 인한 pain point는 감소했다고 할 수 있다고 볼 수 있습니다.
2023년 1월 9일 베타 서비스를 시작으로 편의점 픽업 서비스가 시작되었습니다. 서비스가 오픈하기 전까지 교환권과 관련된 요구 사항이 변경되어 이를 수정하는 일이 있었습니다. 이렇게 매번 제품은 언제든 변경이 필요한 상황에 놓이게 된다는 걸 배웠습니다. 다행히 위에서 말했던 방법으로 지금까지는 이런 변경에 빠른 대응이 가능했습니다.
지금도 그렇지만 입사 당시 프로트 시니어 개발자가 없어, 매순간 내가 가고 있는 방향에 대해 끊임없이 의문을 제시할 수밖에 없었습니다. 하지만, 이런 고민들이 쌓여서 점점 더 나은 개발자로 성장할 수 있을 거라 생각합니다!!
결론은 중고나라 많이 이용해주세요~~ 🥹