Experience/외주

[React/Firebase] 개인 카페 쿠폰 및 고객 관리 프로그램 외주 경험 기록

koh1018 2023. 11. 13. 17:32
반응형

개요

몇 개월 전 진행한 외주에 대한 경험을 기록해두고자 한다.

 

외주를 부탁한 곳은 모 신도시 소재의 개인 카페였다.

 

해당 카페에서는 고객 프로모션 마케팅으로 쿠폰을 발행하고 있었는 데, 11번 주문하면 1회 무료 음료를 제공하는 식이었다.

문제는 고객의 수가 늘어나면서 고객의 정보를 찾기 어려워졌다는 것이다.

이곳은 아마스빈처럼 고객이 쿠폰을 소지하게 하는 것이 아니라 매장에서 직접 쿠폰을 보관하고 관리하는 방식이었는데 고객의 수가 1000명 가까이 되다보니 주문 시 금방금방 찾아 쿠폰을 적립해주기 어려워졌고 특히 동명이인의 경우 어떤 정보가 해당 고객의 정보인지 파악하기 어려워졌다.

 

이러한 이유로 필자에게 외주를 요청하였고 디자이너와 함께 클라이언트의 요구사항에 맞춰 디자인 및 개발까지 완제품으로 프로그램을 만들어주었다.

 

 

개발 스택

처음에 웹 기반으로 만들어야 할 지 앱 기반으로 만들어야 할 지 고민을 많이 했다.

주로 이용하는 디바이스는 태블릿이라고 하였지만 PC로도 이용이 가능하면 좋겠다고 해서 웹 기반으로 만들기로 하였다.

 

개발 시간이 넉넉하지 않았기에 익숙한 Typescript 기반의 서비스를 만들고자 하였고 빠른 퍼블리싱을 위해 tailwindcss를 사용했다.

 

다음으로는 고객의 정보를 저장할 방식에 대한 고민이었는데 우선 후보로 생각한 것은

1. AWS 같은 클라우드 서비스에 백엔드를 구현하기

2. LocalStorage 이용하기

3. Firebase 이용하기

였다.

 

우선 1번은 주어진 개발 시간과 클라이언트의 요구사항을 고려할 때 비효율적인 작업이라고 생각했다.

남은건 2번과 3번이었는데, LocalStorage를 이용하면 인터넷에 연결하지 않아도 사용할 수 있는 장점이 있었지만 하나의 디바이스에서만 사용할 수 있다는점, 데이터 백업이 안된다는 단점이 있었다.

이 때문에 Firebase를 사용하여 간단하게 백엔드를 구현하기로 하였다.

 

 

1차 완성

그렇게 개발을 진행하였고 위와 같이 완성하였다.

검색창과 검색 결과 창을 나누자는 제안이 있었으나 UX를 고려할 때 불필요하게 창을 왔다갔다 이동하는 것보다 메인화면에서 고객을 검색하는 니즈를 충분히 반영할 수 있도록 검색 입력값에 따라 바로바로 검색결과가 보여지면 좋을 것 같다 생각하여 위와 같이 진행하였다.

 

위 화면에서 고객 목록 중 하나를 선택하면 아래와 같은 창이 나온다.

 

비어있는 도장 칸을 클릭하면 도장이 찍어지고 도장 11개를 모으면 쿠폰 1개가 적립되는 방식이다.

 

 

문제 발생

이렇게 완성 후 테스트를 진행하였다.

허나 문제가 발생하였는데, 데이터가 안보이는 문제였다.

일일 최대 읽기 허용량 5만회 초과

이는 위의 보고서와 같이 Firebase의 일일 사용 제한량을 넘어 데이터 호출이 안되는 까닭이었다.

 

따라서 해당 문제를 해결하기 위해 데이터를 호출하는 부분에 대한 코드를 다시 뜯어보았다.

function useCustomerList() {
  const { customerList, setCustomerList } = useCustomerListStore()
  const [isReload, setIsReload] = useState<boolean>(false)

  // 고객 리스트 불러오기
  useEffect(() => {
    if (customerList.length < 1 || isReload) {
      ;(async () => {
        const q = query(customersCollectionRef)

        const querySnapshot = await getDocs(q)
        setCustomerList(querySnapshot.docs.map((doc) => ({
          id: doc.id,
          name: doc.data().name as string,
          lastPhoneNum: doc.data().lastPhoneNum as string,
          stampNum: doc.data().stampNum as number,
          couponNum: doc.data().couponNum as number,
          createdAt: doc.data().createdAt as string
        } as CustomerType)).sort())
      })().finally(() => setIsReload(false))  // 불러오면 리로드 변수 초기화
    }
  }, [isReload])

  return { customerList, setIsReload }
}

export default useCustomerList

위 코드는 고객 리스트를 새로 불러오거나 갱신할 때 사용하기 위해 만든 커스텀 훅이다.

해당 훅을 호출하면 Zustand로 전역 상태 관리중인 customerList를 불러온다.

그리고 만약 해당 값이 비어있다면 Firebase에서 불러오게 된다.

customerList 값이 있는 경우에도 갱신을 위해 다시 불러오고 싶은 경우가 있을 것이다. 그런 경우를 위해 isReload라는 변수를 만들어주었고 해당 값을 외부에서 true로 만들어주는 경우에도 고객 데이터를 다시 불러올 수 있게 만들어주었다.

 

문제는 아래 코드였다.

export const addCustomer = async (name: string, lastPhoneNum: string, setIsReload: Dispatch<SetStateAction<boolean>>) => {
  await setDoc(doc(db, "customers", name + lastPhoneNum), {
    name: name,
    lastPhoneNum: lastPhoneNum,
    stampNum: 0,
    couponNum: 0,
    createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss')
  }).then(() => setIsReload(true))
}

새로운 고객을 추가할 때 사용하는 함수인데 추가할 때마다 setIsReload 함수를 통해 고객 리스트 전체를 다시 불러오도록 하였다.

이 외에도 도장 찍을 때 사용하는 함수등에도 같은 방식의 로직을 사용했었다.

이 때문에 너무 많은 호출이 발생했었고 따라서 일일 사용량을 초과하게 되었다.

 

문제를 해결하기 위해 바로 떠올린 것은 기존에 가지고 있던 값을 재활용하는 것이었다.

즉 새로운 고객이 추가되면 Firebase Store에 반영하고 Firebase Store에서 전체 고객리스트를 다시 불러오는 것이 아니라 Firebase Store에 반영함과 동시에 Zustand에서 물고 있는 고객 리스트 전역 변수값을 같이 수정해주는 방식이었다.

이렇게 수정된 코드는 아래와 같다.

export const addCustomer = async (name: string, lastPhoneNum: string, customerList: CustomerType[], setCustomerList: (customerList: CustomerType[]) => void) => {
  const today: string = dayjs().format('YYYY-MM-DD HH:mm:ss')
  await setDoc(doc(db, "customers", name + lastPhoneNum), {
    name: name,
    lastPhoneNum: lastPhoneNum,
    stampNum: 0,
    couponNum: 0,
    createdAt: today
  }).then(() => {
    setCustomerList([...customerList, {
      id: name + lastPhoneNum,
      name: name,
      lastPhoneNum: lastPhoneNum,
      stampNum: 0,
      couponNum: 0,
      createdAt: today
    }].sort())
  })
}

 

더 이상 setIsReload 함수를 파라미터로 받지 않고 대신 setCustomerList 함수(Zustand의 전역변수 수정 함수)를 활용해 Firebase Store에 반영함과 동시에 전역 변수 리스트도 똑같이 수정되도록 해주었다.

 

 

완성

실제 서비스 중인 현재 상태의 보고서

당시 수정 후 바로 캡처를 못해 시간이 조금 흐른 지금의 그래프이긴 하지만 기존의 5만회 이상 읽기 호출에서 8천회 정도로 기존보다 약 83% 정도 호출 횟수가 줄었다.

 

해당 코드를 수정한 뒤 실제 서비스한 몇 개월의 기간동안 호출량이 일일 제한량을 초과하는 일은 한번도 없었다.

 

현재도 잘 서비스되고 있다.

 

 

 


 

 

위 경험을 통해 프론트에서 개발 시 같은 구현에도 더 적은 호출을 할 수 있으며 이는 백엔드 과부화등 성능에도 영향을 미칠 수 있기 때문에 항상 유념하고 주의해야하는 부분이라는 것을 알게되었다.

평소 많이 듣는 말이긴 했지만 실제로 이 문제 때문에 서비스가 작동이 안된 것은 처음이라 당황했던 것 같다.

또 실제 호출 횟수 그래프를 보니 조금의 비효율적인 코드로 생각보다 큰 성능 차이를 가져올 수 있다는 사실을 직접 눈으로 보게되어 매우 뜻깊었던 경험인 것 같다.

 

해당 개발은 마무리 되었지만 추가로 궁금한 점이 생겨 알아보게 되었는데,

바로 React-Query의 작동 방식이었다.

 

평소 데이터 호출 및 캐싱을 위해 많이 사용하는 라이브러리인데 해당 라이브러리는 어떻게 효율적으로 데이터 호출 수를 줄일지 궁금했다.

 

https://tanstack.com/query/v3/docs/react/overview

 

Overview | TanStack Query Docs

React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze. Motivation Out of the box, React applicatio

tanstack.com

이는 공식 홈페이지의 개요를 보면 알 수 있었다.

바로 캐싱이다.

Zustand와 같은 상태 관리 라이브러리는 클라이언트 상태에서 작업하기는 좋지만 비동기나 서버 상태에서 작업하기는 어렵다. 왜냐면 서버의 state와 다르기 때문이다.

 

이를 서버의 state와 동기화 해주기 위해서(클라이언트단에서 서버 state의 특성을 파악하면서 진행하기 위해서) React-Query는 캐싱을 사용하는데, 먼저 React-Query는 데이터를 fetching 해오면 해당 데이터를 캐싱한다.

그리고 추가로 데이터를 부르면 캐싱된 데이터를 제공하는 것이다.

 

필자가 수정하여 구현한 방법도 유사한 방법이었다. Zustand를 이용하여 캐싱한 것이다.

 

React-Query는 아래와 같은 옵션들로 캐시 사용 시간(staleTime)과 캐시된 데이터가 메모리에 남아있는 시간(cacheTime)을 조정할 수 있게 한다.

refetchOnWindowFocus, // default: true
refetchOnMount, // default: true
refetchOnReconnect, // default: true
staleTime, // default: 0
cacheTime, // default: 5 minutes (60 * 5 * 1000 = 30000)

 

이전에 React-Query를 사용할 때 데이터 호출 횟수에 대해 깊게 생각한 적은 없었는데 앞으로 개발을 진행할 때 항상 캐시를 유념하고 성능을 고려하며 개발해야겠다.

반응형