6. 기술적으로 고민한 것들 - Licruit/Licruit-backend GitHub Wiki

📌 Sentry, ErrorBoundary를 활용한 에러 핸들링

웹 애플리케이션에서는 다양한 이유로 에러가 발생할 수 있습니다.

더 나은 사용자 경험을 유지하고, 시스템 안정성을 확보하기 위해 에러를 적절히 처리하는 것이 중요합니다.

⚠️ 문제상황

  • 명령형 방식으로 에러 처리
  • 개발자마다 다른 에러 핸들링 방식
  • 에러 원인 파악 및 해결에 많은 시간 소비
  • 네트워크 에러, 서버 에러처럼 예측할 수 없는 에러에 대한 핸들링 부족

✔️ 해결방안

sentry를 활용한 에러 추적

KakaoTalk_Photo_2024-09-04-00-22-57

에러가 발생했을 때 저희는 에러가 발생한 지점과 원인을 찾는데 많은 시간을 소모했습니다.

sentry는 에러를 추적하기 위한 프론트엔드 모니터링 툴로, 이를 통해 어플리케이션 전반에서 발생하는 에러에 대해 발생 시점과 상세 정보를 실시간으로 추적하여 에러의 원인을 쉽고 빠르게 확인할 수 있었습니다.

에러 탐지부터 원인 파악, 해결까지의 시간이 대폭 줄어들어 생산성이 향상된 좋은 경험을 얻었습니다.

ErrorBoundary를 활용한 선언적 에러 처리

ErrorBoundary는 React 16에 도입된 특별한 컴포넌트로, ErrorBoundary는 컴포넌트 트리 내의 자식 컴포넌트에서 발생하는 에러를 캐치하고, 해당 에러로 인해 애플리케이션이 중단되지 않도록 도와주는 기능입니다.

전역으로 발생하는, 개발자가 예측할 수 없는 에러를 공통적으로 처리할 수 있으며, 선언적으로 에러를 핸들링할 수 있고, 컴포넌트 단위의 에러 처리도 가능하다는 장점을 가지고 있습니다.

React의 선언적 프로그래밍

React는 선언적 프로그래밍을 지향하고 있습니다.

개발자는 자바스크립트만을 사용할 때처럼 document 객체에서 직접 id 혹은 className, tagName 등으로 요소를 가져와 컨텐츠를 부여하는 방식, 즉 과정에 집중하는 명령형 개발 방식에서 벗어나 원하는 결과에만 집중할 수 있습니다.

같은 맥락에서 저희는 에러 핸들링도 명령형보다는 선언적으로 처리하는 것이 React가 지향하는 프로그래밍 방식에 더 적합하다고 판단하여 ErrorBoundary를 적극적으로 활용하게 되었습니다.

  • ErrorBoundary 도입 전
도입 전에는 비동기 요청에 성공하는 경우와 실패하는 경우가 섞여서 처리되었습니다.

지금 이 코드는 어쩌면 간단해보일지 몰라도 하나의 컴포넌트 내에 비동기 데이터가 2개 이상이 되는 순간 이러한 에러 핸들링의 복잡성은 더욱 커지게 될 것이라 예상했습니다.
  • ErrorBoundary 도입 후
도입 후 컴포넌트는 비동기 요청을 성공한 경우의 결과에만 집중하고, 컴포넌트를 사용하는 상위 컴포넌트에게 에러 처리에 대한 책임을 위임하도록 구현할 수 있었습니다.

저희는 이를 통해 다음과 같은 2가지 이점을 얻었다고 판단했습니다.

- **명확한 책임과 역할 분리:** 성공/실패하는 경우가 분리되어 컴포넌트의 역할이 명확히 드러납니다.
- **선언적 에러 핸들링:** 단순히 Error Boundary를 감싸주고 fallback UI를 등록해주는 것만으로도 에러 핸들링이 가능합니다.
  • cf. Axios interceptor, Tanstack Query의 전역 에러 핸들러

    ErrorBoundary를 제외하고도 에러 처리에 활용할 수 있는 옵션들이 많습니다.

    그 중에서도 Axios interceptor, Tanstack Query의 전역 에러 핸들러를 ErrorBoundary와 함께 고민하였는데, 결과적으로 ErrorBoundary를 선택한 이유는 이 2가지 방법이 지니는 다음과 같은 한계 때문이었습니다.

    • 비동기 통신 에러에 국한된 핸들링

      Axios interceptor와 Tanstack Query는 주로 비동기 처리에 관련된 에러를 처리할 수 있습니다.

      다시 말해 렌더링 중 발생하는 UI 오류, 자바스크립트 오류 등에 대응할 수 없습니다.

    • 명령형 방식

      이 2가지 방식을 이용하면 interceptor와 핸들러 함수 내부에서 명령형으로 에러 처리를 하게 됩니다.

      에러 관련 정보를 로깅하거나 전역적인 에러 상태를 업데이트하고, fallback UI를 보여주는 코드를 직접 작성해야하며, 특히나 interceptor의 경우에는 fallback UI를 보여주기 위해서 navigate 코드를 작성하여 페이지 경로를 이동시켜야하기도 했습니다.

    • 컴포넌트 단위 에러 처리의 한계

      Axios interceptor와 Tanstack Query는 전역 에러 처리에 수월하지만, 컴포넌트 단위의 에러를 처리할 때 어느정도 한계가 존재합니다.

      작은 컴포넌트 하나에서 에러가 발생했다고 해서 전체 화면을 fallback UI로 바꿔줄 필요는 없습니다.

      컴포넌트 단위의 fallback UI를 보여주기 위해서는 에러 상태를 업데이트하여 에러가 발생한 컴포넌트의 UI만 바꿔줄 수 있을테지만, 이런 방법은 여전히 명령형 방식에 해당하고 에러 핸들링 과정의 복잡성이 늘어난다고 판단했습니다.

📌 React-Helmet, Prerenderer을 통한 SEO 최적화

⚠️ 문제 상황

React는 CSR(Client Side Rendering) 방식이기 때문에 SEO 문제가 발생하였습니다. 검색 엔진 봇이 초기 로딩 시점에 페이지의 콘텐츠를 제대로 인식하지 못해, 페이지가 검색 엔진에 제대로 인덱싱되지 않는 상황이 발생했습니다. 그뿐만 아니라 JavaScript가 클라이언트 측에서 실행된 후 페이지가 렌더링되기 때문에, 초기 로딩 속도가 느려지는 문제도 있었습니다.

✔️ 해결 방안

  1. React-Helmet을 통한 동적 메타태그 관리

프로젝트 초기에는 메타태그를 HTML 파일에 직접 추가하는 방식으로 관리하였습니다. 하지만 페이지가 많아지면서 메타태그를 일관성 있게 관리하는 데 어려움이 생겼고, 특정 페이지의 SEO 정보가 누락되는 경우도 있었습니다.

따라서 SEO를 최적화하고 메타태그를 동적으로 관리하기 위해 React-Helmet을 도입하였습니다.

아래 코드처럼 작성하면 페이지마다 고유한 메타 정보를 설정할 수 있고, 이를 통해 검색 엔진이 각 페이지를 개별적으로 인식하며, 정확한 정보 인덱싱도 가능합니다.

  <MetaTag
        title='리크루트 회원가입'
        description='리크루트에 가입하여 다양한 서비스를 이용해 보세요.'
        keywords='리크루트, 회원가입, 비즈니스'
        url='https://www.licruit.site/auth/signUp'
      />
  1. Prerenderer를 사용한 Pre-rendering

: SEO와 초기 로딩 속도를 개선하기 위해 Prerenderer를 사용하여 Pre-rendering을 구현하였습니다. CSR의 단점을 보완하고 검색 엔진이 페이지를 쉽게 크롤링할 수 있도록, 서버 측에서 미리 렌더링된 HTML 페이지를 제공하였습니다.

이를 위해 vite.config.js 파일에 아래와 같은 설정을 추가하였습니다.

   prerender({
        routes: [
          '/',
          '/catalog',
          '/management',
          '/buyingId',
          '/auth',
          '/auth/login',
          '/auth/signUp',
          '/auth/find-password',
          '/catalog',
          '/group-buying',
        ],
        renderer: '@prerenderer/renderer-puppeteer',
        server: {
          host: 'localhost',
          listenHost: 'localhost',
        },
        rendererOptions: {
          maxConcurrentRoutes: 1,
          renderAfterTime: 500,
        },
      }),

Prerender를 사용하여 주요 경로에 대해 미리 렌더링된 HTML 파일을 생성하였습니다.

Prerendering을 통해 사용자가 페이지를 방문할 때 미리 렌더링된 HTML을 제공함으로써 초기 로딩 속도가 현저히 개선되었으며, 검색 엔진이 CSR로 인한 문제 없이 각 페이지의 콘텐츠를 정확하게 인식하고 인덱싱할 수 있게 되었습니다.

📌 주류 카탈로그 검색 성능 개선

⚠️ 문제 상황

문제 1

주류 카탈로그 조회에서 주류 이름 검색을 위해 LIKE를 사용했을 때 Table Full Scan이 발생하여 쿼리 성능이 저하되는 문제가 있었습니다.

문제 2

별도의 설정 없이 Full-Text Index를 생성했더니 검색어가 단어 중간에 위치하면 검색이 되지 않는 문제가 발생했습니다.

문제 3

Full-Text Index와 리뷰 평점 정렬 기능을 위한 ORDER BY 절을 함께 사용하면 속도가 느려지는 문제를 발견했습니다. FROM 절에서 서브 쿼리를 사용하면 속도가 느려지는 문제는 해결할 수 있지만 현재 프로젝트에서 사용하고 있는 ORM인 sequelize에서는 FROM 절에 서브 쿼리를 사용하기 어렵고, 코드가 복잡해지는 문제가 있었습니다.

✔️ 해결 방안

문제 1 해결

주류 이름 컬럼에 Full-Text Index를 생성하여 주류 이름을 검색할 때 Table Full Scan이 일어나지 않도록 하여 Query Cost를 낮췄습니다.

문제 2 해결

별도의 설정 없이 Full-Text Index를 생성하면 Parser가 Built-In으로 설정됩니다. Built-In Parser는 구분자(공백, 문장 기호, 사용자가 지정한 특정 단어 등)를 기준으로 Tokenizing을 합니다. 따라서 검색 키워드가 정확히 일치하거나 전방 일치하는 경우에만 결과를 받을 수 있기 때문에 문제 2가 발생했습니다.

반면 N-gram Parser는 지정된 토큰 사이즈를 기준으로 키워드를 추출하기 때문에 검색어가 단어의 중간에 들어가도 검색이 가능합니다. 따라서 기존에 Built-In Parser로 생성했던 Full-Text Index를 삭제하고, N-gram Parser인 Full-Text Index를 새로 생성하여 검색어가 단어의 중간에 들어가도 검색될 수 있도록 했습니다.

문제 3 해결

Full-Text Index와 ORDER BY 절을 함께 사용하면 데이터 양이 적은 상황임에도 속도가 3배 정도 느려지는 문제가 있었습니다. Full-Text Index 관련 검색을 FROM 절에서 서브 쿼리로 수행한 후에 정렬을 하면 속도가 느려지는 문제는 해결할 수 있지만, sequelize에서는 FROM 절에 서브 쿼리를 사용하기 어렵고, raw 쿼리를 사용하려 해도 다른 필터링 조건들이 있기 때문에 코드가 복잡해지는 문제를 피할 수 없었습니다. 현재 상태에서는 LIKE를 사용해도 큰 성능 저하가 나타나지 않기 때문에 검색 조건과 리뷰 정렬이 동시에 이루어져야 할 때만 LIKE를 사용합니다.

📌 OCR을 통한 사업자등록증 검증

⚠️ 문제 상황

회원가입 시 사업자임을 확인할 수 있는 방법과 주류 도매업체 여부가 필요합니다. 사업자등록번호는 검색을 통해 누구나 알 수 있기 때문에 국세청에서 제공하는 사업자등록정보 관련 API로 사업자 본인임을 판별하기에는 문제가 있습니다. 또한, 회원가입할 때 도매업체로 가입한다면 이를 수기로 승인하기 위한 별도의 Admin 페이지가 필요하다는 문제가 있었습니다.

✔️ 해결 방안

네이버 클라우드 플랫폼의 CLOVA OCR 중 사업자등록증 특화 모델을 사용하여 사업자등록증에 기재된 정보를 추출하는 방법을 선택했습니다.

OCR 도메인을 생성하여 사업자등록증 특화 모델 사용 승인을 받은 후, API Gateway를 생성하여 OCR 도메인을 연결합니다. 생성한 API Gateway URL로 사업자등록증 이미지와 비밀키를 보내서 사업자등록증 판독 결과를 응답으로 받습니다.

응답에서 문서 유형이 사업자등록증이 맞는지 확인하고, 사업자 번호와 업종 필드가 존재하는지 확인합니다. 업종에 “주류 도매업”이 있을 경우 자동으로 도매업체 권한을 부여하도록 구현했기 때문에 Admin 페이지에서 수기로 승인해야 하는 문제를 해결할 수 있었습니다.