데모 발표 자료 #366
sgoldenbird
started this conversation in
Discussions
데모 발표 자료
#366
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
시은
지금까지 전체 문서
TIL
Trouble Shooting
UX improvement
Engineering Breakdown
--> 로컬스토리지 → HTTP-only 쿠키: 인증 보안을 위해 아키텍처 개선 #134 연관
--> #286 연관
--> Next.js BFF 환경에서 카카오 OAuth 인가 코드와 닉네임을 동시에 전달할 수 없는 문제 해결 #306 연관
--> `카카오 소셜 로그인, 회원가입 흐름 설계 전략`: 인가코드 일회성, 분리된 백엔드, UX 혼란 해결 #500 연관
데모 발표
회원가입, 로그인
로컬스토리지 → HTTP-only 쿠키: 인증 보안을 위해 아키텍처 개선
기존 프로젝트는 프론트엔드(클라이언트 사이드)에서 백엔드 API를 직접 호출하고, 로그인 후 받은 인증 토큰을 로컬 스토리지 등 클라이언트 환경에서 직접 관리하는 구조였습니다.
이러한 방식은 다음과 같은 문제점을 야기했습니다.
로컬 스토리지에 저장된 토큰은 XSS(Cross-Site Scripting) 공격에 취약하여 탈취 위험이 있었습니다.
모든 클라이언트 요청에서 토큰을 수동으로 Authorization 헤더에 추가해야 했으며, 토큰 갱신 로직이 여러 곳에 분산될 수 있어 관리 및 유지보수가 복잡했습니다.
클라이언트가 백엔드 API의 실제 엔드포인트를 직접 호출하므로, 백엔드 서비스의 구조가 클라이언트에 노출되는 단점이 있었습니다.
이러한 문제들을 해결하고 보안을 강화하며 인증 관련 로직을 중앙 집중화하기 위해, Next.js API Route를 BFF(Backend for Frontend) 프록시로 활용하는 아키텍처로 전환하였습니다.
개선한 구조 (Next.js API Route, Middleware, BFF)
[Next.js API Route (src/app/api/auth/**/route.ts 등)]:
[Middleware (src/middleware.ts)]:
Next.js Middleware는 config.matcher에 설정된 /api/:path* 요청(단, /api/test/ 및 /api/auth/ 경로 제외)에 대해 다음과 같은 역할을 수행합니다.
클라이언트가 api/ 접두사로 요청을 보내면, Middleware는 이 요청의 경로에서 /api 접두사를 제거하고 실제 백엔드 API의 URL에 연결하여 백엔드 API로 직접 요청을 보냅니다. 이로써 클라이언트의 브라우저에는 실제 백엔드 엔드포인트가 노출되지 않으며, Next.js Middleware가 BFF 역할을 직접 수행합니다.
BFF 구조 장점
사용자 회원가입 및 자동 로그인
POST /api/auth/signup요청을authApiClient를 통해 보냅니다./api/auth/로 시작하므로, 아무런 처리 없이 해당 Auth API Route로 요청을 그대로 통과(Bypass)시킵니다./api/auth/signup/route.ts): 요청을 받아 백엔드 인증 서버로 전달합니다.accessToken,refreshToken)을 응답합니다.src/domain/Auth/utils/setAuthCookies.ts를 사용하여HttpOnly쿠키로 설정하고 클라이언트에 최종 응답합니다.OAuth(Kakao) 흐름
state파라미터(로그인/회원가입 의도 정보 포함)를 포함하여 카카오 인증 페이지로 리디렉션합니다.code와state를 포함하여 우리 서버의/api/auth/kakao/callback으로 리디렉션합니다./api/auth/kakao/callback/route.ts):code를 이용해 카카오accessToken을 얻고, 이를 이용해 사용자 정보를 조회합니다.state파라미터를 분석하여 사용자가 로그인을 시도했는지 회원가입을 시도했는지 분기 처리합니다.accessToken,refreshToken을 발급받습니다.src/domain/Auth/utils/setAuthCookies.ts를 통해 클라이언트 쿠키에 설정하고 메인 페이지(/)로 리디렉션합니다.state파라미터를 통해 사용자의 최초 의도를 정확히 파악하여 소셜 로그인/회원가입 절차를 자동화하고, 불필요한 재인증 없이 즉시 서비스 이용이 가능하도록 합니다.에러 처리 아키텍처
에러 처리 아키텍처
Next.js App Router 기반 RoamReady 프로젝트에서 발생 가능한 다양한 에러를 구조적으로 처리하고, 사용자 경험을 보호하기 위해 다층 에러 처리 구조를 설계했습니다.
폴더 구조로 본 에러 처리 흐름
계층별 에러 처리
src/app/error.tsx는 앱 전체의 마지막 방어선으로, 치명적 오류 발생 시 전체 UI를 대체합니다.src/app/(app)/error.tsx는 특정 그룹 내 에러만 처리해 상위 레이아웃을 보존하며, UX 일관성을 유지합니다.앱 전체가 아닌 특정 영역에서만 문제가 발생했을 때 React 클래스 컴포넌트 기반의 ErrorBoundary로 오류를 격리합니다.
외부 라이브러리 대신 직접 클래스를 구현한 이유는 다음과 같습니다:
apiClient.ts에서ky의 훅을 사용해 JSON 파싱, 스키마 유효성 검사 등을 수행하고, 실패 시 사용자에게 메시지를 전달합니다.middleware.ts가 먼저 감지하여 자동 토큰 재발급을 시도하고, 실패 시AuthStatusProvider가 세션 만료를 감지해 사용자 상태를 초기화하고 로그인 페이지로 이동시킵니다.useToast().showError()등으로 사용자에게 메시지를 표시합니다.API 에러 처리 5 단계 흐름
로딩쪽은 구체적인 코드나 어디에 어떻게 사용했는지는 추후 업데이트 필요해 보임.로딩 처리
테스트 처리
UI 테스트 페이지 (page.dev.tsx) → pageExtensions 기반 조건부 포함
next.config.ts 파일에서 NODE_ENV 또는 VERCEL_ENV 환경 변수를 확인하여 개발 또는 프리뷰 환경일 때만 dev.tsx 확장자를 pageExtensions 설정에 추가하도록 설정했습니다. 즉, .dev.tsx확장자를 페이지로 인식하도록 구성했습니다.
이 설정 덕분에 test/ 디렉토리 내의 page.dev.tsx 파일은 개발 또는 Vercel 프리뷰 환경에서만 페이지로 인식되고, 프로덕션 빌드에서는 제외됩니다.
API 에러 테스트 라우트 (route.ts) → 런타임 조건부 비활성화
Next.js App Router는 파일 시스템 기반 라우팅을 사용하며, route.ts와 같은 파일은 특정 경로에 대한 API 엔드포인트로 자동 인식됩니다.
이러한 API 라우트 파일은 pageExtensions 설정의 영향을 받지 않기 때문에, *.dev.ts와 같이 커스텀 확장자를 붙여도 빌드에서 제외할 수 없습니다.
즉, api/test/error/route.ts와 같은 API 라우트는 무조건 빌드에 포함되며, App Router의 기본 동작 방식상 조건부로 제외하는 메커니즘이 존재하지 않습니다.
따라서 빌드에는 포함시키되 실행 시점에 환경 변수(process.env.NODE_ENV)를 확인하여 프로덕션 환경에서는 해당 라우트의 기능을 비활성화하는 방법을 사용했습니다. 이는 파일 자체를 빌드에서 제외하는 대신, 런타임에서 로직을 제어하는 방식입니다.
이 방식은 아래와 같이 동작합니다.
결론적으로, API 테스트 라우트는 빌드에 포함하되, 런타임에서 동작을 조건부 제어하여 안전하게 운영 환경을 보호할 수 있습니다.
UX Improvement
클라이언트 측 비밀번호 확인 로직 도입 (Zod refine 활용)
기존에 서버에서 담당하도록 설계되었던 회원가입 시 비밀번호 일치 여부 검증 로직을 클라이언트 측(프론트엔드)에서 처리하도록 변경하여 사용자 경험을 개선하고 서버 부하를 줄였습니다. 이를 위해 Zod 라이브러리의
refine메서드를 활용하여 스키마 기반 유효성 검사를 적용했습니다.기존 시나리오:
기존 설계에서 비밀번호 일치 검사를 서버에 의존함으로써 다음과 같은 문제가 발생했습니다:
개선된 시나리오:
onBlur), 비밀번호와 비밀번호 확인 필드의 값이 일치하는지 클라이언트에서 즉시 검사합니다.구현 구조
사용자가 입력한 비밀번호(password)와 비밀번호 확인(passwordConfirm)이 일치하는지 프론트엔드에서 유효성 검사하고 서버로 데이터를 전송할 때는 passwordConfirm 필드를 제외하고 전송
1. Zod 스키마 정의 (
src/domain/Auth/schemas/request.ts):기본 스키마 (baseSignupRequestSchema)
refine 추가 (signupRequestSchema)
refine()메서드를 사용하여 password와 passwordConfirm 값의 일치 여부를 검사2. 타입 추론 및 구분:
3. 폼 컴포넌트 통합 (/src/domain/Auth/components/SignUpForm.tsx):
register()적용서연
도메인
상세페이지 구현 사항
체험 상세페이지는 초기 로딩 속도와 검색 엔진 최적화를 고려하여 전체를 서버 컴포넌트(Server Component) 기반으로 구성했습니다.
정적 성능을 확보하기 위해, 체험 상세 데이터는 ISR(Incremental Static Regeneration) 방식으로 처리되며, Next.js의
revalidateTag()를 통해 특정 조건 하에 최신 데이터로 갱신될 수 있도록 했습니다.하지만 예약 기능과 같이 사용자 입력 및 상태 변화가 필요한 영역은 서버 컴포넌트 안에서 바로 구현할 수 없었기 때문에, 해당 영역은 클라이언트 컴포넌트로 분리하여 Wrapper 방식으로 감쌌습니다. 이를 통해 서버 렌더링의 장점은 유지하면서도 클라이언트와의 상호작용이 가능하게 설계했습니다.
리뷰 데이터는 다음과 같은 전략을 사용했습니다:
invalidateQuery()를 통해 리뷰 목록을 최신화할 수 있도록 했으며,예약 기능은 디바이스 유형에 따라 UI 구조를 유동적으로 변경하는 반응형 구조를 도입했습니다.
모바일, 태블릿, PC 각각의 환경을
useMediaQuery로 구분하여 사용자 경험을 최적화할 수 있도록, 예약 상태를 유지할 수 있는 Wrapper 컴포넌트를 상단에 두어 상태가 통일하게 관리되도록 했습니다.또한, 예약 가능 날짜는
available-scheduleAPI를 통해 가져오며, 다음과 같은 방식으로 처리합니다invalidateQuery()를 통해 날짜 데이터가 갱신되도록 처리했습니다.사용자 경험(UX) 측면에서도 여러 개선을 적용했습니다.
체험 상세 이미지에는 클릭 시 전체 화면 확대되는 모달 이미지 뷰어를 도입하고,
을 추가해 사용자가 다양한 각도에서 체험 이미지를 탐색할 수 있도록 개선했습니다.
또한 예약 기능에는 다음과 같은 예외 케이스를 처리했습니다.
이처럼 다양한 사용자 상황을 고려해 정확하고 명확한 피드백을 제공하는 방향으로 개선했습니다.
알림 주기적 반영
현재 백엔드에서는 WebSocket이나 SSE와 같은 실시간 알림 시스템을 제공하지 않기 때문에,
프론트엔드에서는 10초 간격의 polling 방식을 활용하여 주기적으로 알림 데이터를 가져오는 구조로 구현했습니다.
단순한 polling만으로는 사용자 입장에서 실시간성을 느끼기 어렵기 때문에, localStorage를 함께 사용하여 UX를 개선했습니다.
처음 오는 알림 데이터를 localStorage에 저장해두고, 새롭게 가져온 알림과 비교해 신규 알림이 존재할 경우
bell-dotUI를 표시하여 사용자에게 즉각적인 시각적 피드백을 제공합니다.이 방식을 통해 다음과 같은 효과를 얻을 수 있었습니다.
공통
Next.js 15의 Route Handler 및 Page Params 처리 경험
Next.js 15 버전부터 App Router 기반의 구조에서는
params가 기존과 달리 동기 객체가 아닌 Promise 객체로 전달된다는 점을 실제 구현 과정에서 확인할 수 있었습니다.처음에는 route handler에서
params를 일반 객체로 간주하고 바로 구조 분해하려 했으나, 런타임에서undefined관련 오류가 발생했습니다. 이는params가 아직 resolve되지 않은 상태에서 접근하려 했기 때문이었습니다.실제 에러 메시지와 공식 문서를 통해 문제의 원인을 분석한 결과, Next.js가 서버 측에서 동적 세그먼트를 비동기적으로 처리하도록 구조를 변경하면서
params를Promise형태로 넘기도록 설계한 것을 알게 되었습니다.따라서 아래와 같이 구조 분해를 사용하기 전에 먼저
await를 통해params를 resolve하는 방식으로 수정해야만 정상적으로 동작했습니다.비슷한 문제는 페이지 컴포넌트 (
page.tsx)에서도 발생했습니다.params를{ id: string }처럼 선언하고 바로 구조 분해했을 경우에도 동일한undefined오류가 발생했는데, 이 역시params가Promise형태이기 때문이었습니다. 이를 해결하기 위해params를Promise<{ id: string }>타입으로 명시하고, 내부에서await처리함으로써 오류 없이id값을 안전하게 추출할 수 있었습니다.이번 경험을 통해 타입 정의가 프레임워크 내부 동작과 어긋날 경우, 빌드 타임에 에러가 발생할 수 있다는 점, 그리고 프레임워크 내부에서 어떤 방식으로 데이터를 전달하고 처리하는지를 정확히 이해하는 것이 중요하다는 점을 배웠습니다.
특히 Next.js와 같은 프레임워크는 버전 업그레이드 시 주요 동작 방식이 바뀔 수 있기 때문에, 공식 문서를 항상 먼저 확인하고 새로운 패턴에 맞춰 코드를 작성해야 한다는 점을 체감했습니다.
또한, 문제가 발생했을 때 AI에 의존하거나 타입스크립트 오류에만 집중하기보다는, 콘솔 로그를 통해 실제 전달 값이 어떤지 직접 확인하는 것이 중요하다는 교훈도 얻었습니다. 앞으로는
params와 같은 기본 객체도 무조건 await 처리를 고려하고, 내부 구조가 동기인지 비동기인지 확인하는 습관을 가져야겠다는 생각이 들었으며, 이와 같은 작은 실수 하나가 전체 페이지의 빌드 실패로 이어질 수 있다는 점에서, 더욱 꼼꼼한 코드 작성과 검증의 중요성을 느낄 수 있었습니다.용민
#도메인 - 달력 예약 현황 페이지
조건부 API 호출 및 ISR 도입 이유
Next.js의 다양한 렌더링 방식 중 ISR(Incremental Static Regeneration)을 선택한 이유는 성능과 사용자 경험의 균형을 맞추기 위함입니다.
초기 체험 목록 페이지는 상대적으로 정적인 정보이므로, SSG로 미리 빌드하고 필요 시 특정 페이지만 ISR로 갱신하여 빠른 초기 로딩 속도와 SEO를 확보했습니다.
이후 데이터 흐름은 사용자의 상호작용에 따라 Lazy Loading 방식으로 구성되었습니다. 예:
이때 TanStack Query의 enabled 옵션을 적극 활용하여, "필요할 때만 불러오는 조건부 API 호출" 패턴을 구현했습니다.
이를 통해 불필요한 네트워크 요청을 최소화하고, 필요한 데이터만 시점에 맞춰 동적으로 불러와 퍼포먼스를 최적화하였습니다.
하나 승인하면 나머지는 자동 거절 - 비즈니스 로직 구현
예약 승인은 단순 상태 변경이 아닌, 실제 비즈니스 운영 방식에 맞춘 복합 로직입니다.
사용자가 특정 예약을 승인하면, 같은 스케줄(scheduleId)에 포함된 다른 모든 예약은 자동으로 거절 처리됩니다.
이를 위해 다음과 같은 로직을 구현했습니다:
승인 대상 예약의 scheduleId 기반으로 전체 예약 목록 조회
승인 예약 외의 ID만 필터링하여 거절 처리
Promise.all로 병렬 API 호출 → 처리 속도 향상
일부 API 실패 가능성을 고려해 예외 처리 및 롤백 전략을 추가하였으며,
완료 후 관련 쿼리 키를 정확히 지정하여 캐시 무효화 → UI 일관성 유지
반응형 UI를 위한 공통 컴포넌트 전략
모바일/데스크톱 환경에 따른 다른 레이아웃 요구를 하나의 컴포넌트 구조로 해결했습니다.
레이아웃 컨테이너만 다르고, 내부 로직 및 UI는 모두 동일한 컴포넌트를 공유합니다.
데스크톱: Popover에 예약 상세 표시
모바일: BottomSheet에 예약 상세 표시
디바이스 감지는 useMediaQuery 훅으로 처리하고, 조건 분기로 각기 다른 래퍼를 사용합니다.
상태, 이벤트 핸들러, UI 구성 요소들은 완전히 동일하게 유지되어, 중복 없는 유지보수 가능한 구조를 확보했습니다.
Lazy Loading + 캐싱 최적화
예약 데이터는 필요한 순간에만 불러오고, 불필요한 요청은 방지하는 전략으로 구성했습니다.
API 요청은 다단계 조건 하에 실행되며, 아래 조건이 만족되어야 호출됩니다:
이는 TanStack Query의 enabled, select 등을 활용해 구성
이미 요청된 데이터는 캐시에 저장되어 중복 요청 없이 재사용되고,
요청 순서와 관계없이 사용자 인터랙션이 매끄럽게 이어질 수 있도록 설계
예약 상태의 우선순위 기반 시각화
같은 날짜에 여러 예약 상태가 있을 수 있기 때문에, 사용자에게 중요한 정보를 먼저 보여주는 정렬 시스템을 구현했습니다.
상태 우선순위: 신청(pending) > 승인(confirmed) > 거절(declined)
각 날짜 셀에 표시되는 아이콘은 이 우선순위 기반으로 정렬되며,
색상 + 텍스트 + 숫자를 함께 사용해 색각 이상 사용자도 정보를 명확히 구분할 수 있도록 설계
모바일에서는 제한된 공간을 고려해 우선순위가 높은 상태만 먼저 노출, 나머지는 "더보기" 형식으로 처리
캐시 무효화를 통한 실시간 동기화
예약 승인/거절 등 상태 변경 후, 즉각적으로 관련 UI들이 최신 상태로 반영되도록 캐시 무효화 전략을 도입했습니다.
상태 변경 시, 영향을 받는 쿼리 키들:
'reservationsBySchedule', 'schedules', 'reservationDashboard' 등을 invalidateQueries로 무효화
모든 UI가 새로고침 없이 자동 갱신되어 사용자는 일관된 데이터를 경험
낙관적 업데이트를 함께 사용해 클릭 즉시 반응하도록 UX 최적화
실패 시 자동 롤백 → 안전성 확보
도메인 - 체험 등록/수정
클라이언트 중심의 폼 구조와 데이터 처리
체험 등록/수정 폼은 정적 콘텐츠가 없는 완전한 사용자 입력 페이지입니다. 기사나 상품처럼 서버에서 미리 만들어 보여줄 콘텐츠가 없기 때문에, SSR이나 SSG로는 얻을 수 있는 이점이 거의 없고, 오히려 동적 인터랙션에 적합한 **CSR(SPA 방식)**이 자연스럽게 선택되었습니다.
또한, 이 폼은 실시간 검증, 이미지 미리보기, 조건부 API 호출, 스케줄 동적 처리 등 클라이언트에서 즉시 처리되어야 할 동작이 많기 때문에, Next.js의 클라이언트 컴포넌트 기반 아키텍처로 구성되었습니다.
특히 폼 작성 중 이탈 방지 기능을 구현하기 위해 브라우저의 히스토리 API와 이벤트 리스너가 필요했으며, 이는 CSR 방식에서만 가능한 기능이었습니다.
컴포넌트 분리와 상태 관리
폼은 제목, 설명, 이미지, 가격, 위치, 스케줄 등 다양한 입력 항목으로 구성되어 있고, 이 모든 항목을 하나의 컴포넌트로 묶으면 유지보수나 테스트가 매우 어렵습니다. 따라서 각 필드를 독립된 컴포넌트로 나누어 단일 책임 원칙을 적용했고, 등록/수정 페이지 모두에서 공통 컴포넌트를 재사용했습니다.
전체 상태 관리는 react-hook-form과 FormProvider를 기반으로 하여, 입력값 추적, 검증, dirty 상태 감지 등을 효율적으로 처리했습니다. 이 구조 덕분에 부모 컴포넌트는 전체 상태를 일관되게 관리할 수 있었고, 자식 컴포넌트는 본인의 책임에만 집중할 수 있었습니다.
사용자 입력 보호: 폼 변경 감지와 이탈 방지
체험 등록 폼은 작성 도중 실수로 페이지를 벗어나면 사용자 데이터가 사라질 위험이 큽니다. 이를 방지하기 위해 react-hook-form의 isDirty 상태를 활용하여 사용자가 입력을 시작한 시점부터 변경 여부를 추적했고, 브라우저 히스토리를 조작하여 뒤로가기 감지 및 커스텀 다이얼로그를 띄웠습니다.
beforeunload 이벤트도 함께 사용하여 브라우저 닫기 시점에도 이탈을 막을 수 있도록 했으며, 사용자에게 명확한 "정말 떠나시겠습니까?" 다이얼로그를 통해 데이터를 잃지 않도록 했습니다.
실시간 유효성 검증으로 사용자 피드백 강화
검증은 단순히 제출 직전에만 하는 것이 아니라, 입력 중간중간 즉각적으로 피드백을 주는 것이 중요합니다. 이를 위해 zod 스키마를 기반으로 입력 규칙(예: 제목은 1~50자, 가격은 양수 등)을 정의하고, react-hook-form과 연동하여 onBlur 또는 입력 시점에 즉시 검증이 이루어지도록 구성했습니다.
에러 메시지는 사용자 친화적인 문장으로 구성하고, 시각적으로도 경고 표시(예: 빨간 테두리)를 통해 사용자가 실수한 부분을 즉시 인지할 수 있도록 했습니다.
이미지 업로드 처리 최적화
이미지 입력은 단순 텍스트와 달리, 미리보기, 메모리 누수 관리, 지연 업로드, 삭제 상태 처리 등 여러 과제가 복합적으로 얽혀 있습니다. 다음과 같은 전략으로 해결했습니다:
스케줄 처리의 효율성과 정확성
체험의 시간표는 사용자에 따라 복수 개 입력이 가능해야 하며, 중복된 일정은 허용되지 않아야 합니다. 이를 위해 useFieldArray를 사용하여 필드 동적 추가/삭제가 가능하게 했고, Set 자료구조를 활용하여 O(1) 시간 복잡도로 중복 일정 검사를 수행했습니다.
스케줄 입력 시, 시작/종료 시간이 비어 있거나 논리적으로 맞지 않는 경우 에러를 표시하여 데이터 품질을 보장했습니다.
네트워크 및 API 처리 최적화
입력값을 모두 서버로 보내는 방식은 비효율적이므로, 스마트 업데이트 전략을 적용했습니다. 즉, 폼이 로드될 때 원본 데이터를 저장하고, 제출 시 현재 값과 비교하여 실제 변경된 필드만 서버에 전송하도록 했습니다.
이 방식은 네트워크 부하를 줄이고, 서버에서의 처리 시간을 단축시키며, 동시에 불필요한 상태 변경이나 리렌더링을 막는 데 효과적입니다.
데이터 무결성: 스키마와 상태 일치
폼 입력의 신뢰성을 높이기 위해, 런타임에서도 타입 검증이 가능한 Zod 스키마를 활용하여 모든 필드를 정밀하게 검증했습니다. 이와 함께, isDirty 및 컴포넌트 간 콜백 통신을 통해 모든 상태 변경을 부모가 일관되게 인지하고 제어할 수 있도록 설계했습니다.
이 구조 덕분에, 사용자가 입력한 데이터는 항상 예상 가능한 형태로 유지되며, 잘못된 값이 서버로 넘어가는 일이 발생하지 않도록 방지할 수 있었습니다.
재현
프로젝트 수행 절차 및 방법 (기능 소개)
메인페이지
메인페이지 데이터 페칭 방식
ActivityCarousel
calc()함수를 적용하여 레이아웃 계산ActivitySection 및 ActivityFilter
useSearchParams와useRouter를 이용하여 API 요청, 필터 상태 및 URL을 동기화prefetchQuery기반 프리페칭한 데이터로 초기 로딩 시간을 확보하고, 이후 필터 및 페이지네이션에는 클라이언트 사이드에서 작동하도록 구성ActivitySearchBar
검색 결과 페이지
검색 결과 페이지 데이터 페칭 방식
useInfiniteQuery를 활용하여 무한 스크롤 구현IntersectionObserver를 활용하여 마지막 요소가 감지되면 자동으로 다음 페이지 로딩useSearchParamsAPI를 이용하여 검색 파라미터 접근을 위해Suspense바운더리를 설정하여 안전하게 비동기 처리반응형 레이아웃
useBreakpoint훅을 통해MobileSearchView,TabletSearchView,DesktopSearchView로 분리하여 디바이스 크기 별로 컨텐츠가 완전히 바뀌도록 분리결과 렌더링 및 Filter
지도 기능 (미정)
마이페이지
반응형 레이아웃
마이페이지 메뉴
usePathnameAPI를 이용하여path.startWith(href + '/')를 통해 중첩 라우팅 구조에서도 정확하게 부모 메뉴를 활성화하도록 구현motion/react라이브러리의Spring애니메이션을 적용하여 부드러운 사용자 인터랙션 구현내 정보 수정
예약 내역
내 체험 관리
자체 평가 의견 (Trouble Shooting / Engineering Breakdown)
메인페이지
액티비티 탐색 기능이 기존의 키워드 검색, 카테고리 필터, 정렬 기능만으로는 사용자에게 충분히 개인화된 경험을 제공하기 어렵다고 판단했습니다. 사용자가 원하는 액티비티를 보다 신속하고 정확하게 찾을 수 있도록 사용자 경험 개선을 목표로 삼았습니다.
O(N+1) API 호출 문제를 O(1)로 개선한 경험: Cloudflare 기반 ETL 전략
가장 중요한 검색 조건인 날짜와 지역 정보를 추가하고자 했으나, 백엔드 API가 날짜와 지역으로 직접 필터링하는 기능을 지원하지 않아 제한이 있었습니다. 기본 액티비티 목록은
GET /activitiesAPI에서 주소 정보를 포함해 제공되었지만, 날짜 정보는 각 액티비티의 상세 조회 API(GET /activities/{id})를 통해서만 확인할 수 있었습니다.초기에는 전체 액티비티 목록을 가져와 주소(
address) 기준으로 1차 필터링한 후, 필터링된 각 액티비티에 대해 상세 API를 개별 호출하여 날짜 조건을 추가로 확인하는 방식을 고려했습니다. 하지만 이 방법은 액티비티 수에 따라 API 호출 횟수가 급격히 증가하여, 사용자 입장에서는 응답 시간이 크게 지연되는 문제를 초래했습니다.이 문제를 해결하기 위해 Cloudflare를 활용한 데이터 사전 가공(ETL: Extract, Transform, Load) 전략을 설계하고 직접 구현했습니다.
Cloudflare Workers에서 주기적으로 실행되는 스크립트를 작성해, 백엔드 API에서 전체 액티비티 목록과 상세 일정 정보를 모두 추출했습니다. 이후 두 데이터를 메모리 내에서 병합해 각 액티비티에 모든 일정 정보가 포함된 단일 JSON 구조로 통합했으며, 이 통합 데이터를 Cloudflare KV Store에 저장해 엣지 네트워크를 통해 빠르게 조회할 수 있도록 했습니다. 작업은 10~15분 간격으로 자동 실행되도록 설정해 데이터 신선도를 유지했으며, 별도의 엔드포인트를 추가해 필요 시 수동으로 데이터 갱신을 트리거할 수 있도록 구현했습니다.
그 결과, 사용자의 검색 요청 시 평균 10초가량 소요되던 처리 시간을 약 1.4초로 대폭 단축시켜, 응답 속도와 사용자 경험 모두 크게 개선할 수 있었습니다.
구체적인 구현 과정
Extract: Cloudflare Workers를 활용하여 주기적으로 실행되는 스크립트를 개발했습니다. 이 스크립트는 백엔드 API의 GET /activities 엔드포인트를 호출하여 모든 액티비티의 기본 목록을 추출했습니다. 이어서, 추출된 각 액티비티의 id를 사용하여 GET /activities/{id} 엔드포인트를 개별적으로 호출, 해당 액티비티의 상세 정보(특히 schedules 필드)를 모두 추출했습니다.
Transform: 추출된 두 가지 유형의 데이터를 메모리 상에서 하나의 통합된 데이터 구조로 변환하고 병합했습니다. 즉, 각 액티비티 객체 안에 해당 액티비티의 모든 날짜(스케줄) 정보가 포함되도록 재구성했습니다. 이렇게 통합된 데이터는 검색 및 필터링에 최적화된 단일 JSON 파일 형태로 완성되었습니다.
Load: 변환 및 통합이 완료된 JSON 파일을 Cloudflare KV Store에 저장했습니다. Cloudflare KV Store는 Edge 네트워크에 데이터를 분산 저장하여 초고속 데이터 읽기를 지원합니다. 이 과정은 Cloudflare Workers의 Cron Trigger 기능을 사용하여 10~15분마다 자동으로 실행되도록 설정했습니다. 이는 백엔드 데이터의 최신성을 일정 주기마다 반영하면서도, 실제 사용자 요청 시에는 항상 사전 가공된 데이터를 제공하여 API 호출 부하를 최소화하기 위함입니다.
클라이언트 레벨에서 자동완성 구현하기
실서비스에 가까운 사용자 경험을 제공하고자 검색 기능에 자동완성 기능을 도입했습니다. 기존에는 사용자가 입력한 문자열을 그대로 백엔드에 쿼리하는 구조였기 때문에 오탈자나 연관 키워드에 유연하게 대응하지 못하는 한계가 있었습니다. 이에 따라 사용자가 원하는 검색 결과에 도달하기 어렵다는 문제가 있었고, 이를 해결하기 위해 ‘키워드 검색’과 ‘위치 검색’ 자동완성 기능을 구현했습니다.
먼저, 기존에 구축해 두었던 Cloudflare Workers 기반 ETL 파이프라인을 확장해 자동완성 전용 데이터셋을 구축했습니다. 주기적으로 백엔드 API에서 전체 액티비티 데이터를 수집한 뒤, 각 액티비티의
title,category,description필드를 기준으로 자동완성 후보어를 추출했습니다.추출된 텍스트는 JavaScript 내장 문자열 처리 메서드(
replace(),split(),trim()등)와 정규표현식을 활용해 특수문자와 불필요한 숫자를 제거한 후, 공백을 기준으로 단어를 분리하고Set자료구조로 중복을 제거했습니다. 이후에는 "체험", "투어", "클래스" 등 실제 검색에서 자주 활용되는 핵심 키워드를 수작업으로 보강하여 추천 품질을 높였습니다.한글 특성상 자소 단위 검색에 대응해야 했기 때문에,
es-hangul라이브러리를 활용하여 초성·중성 기반 검색 매칭을 구현하여 사용자 입력이 완전하지 않더라도 연관 키워드를 추천해주도록 했습니다.또한 추천 속도 개선을 위해 정제된 단어 목록은 JSON 배열로 Cloudflare KV에 저장하고, 클라이언트 최초 로딩 시점에
fetch하여 메모리에 적재한 후, 클라이언트에서 입력 이벤트에 따라 필터링·추천하는 구조로 구현했습니다. 이를 통해 API 요청 없이도10ms이내의 추천 응답 속도를 달성했습니다.위치 자동완성의 경우, 직접적인 주소 데이터 확보가 어렵고 정합성이 중요한 만큼, 카카오 로컬 API의 주소 검색 기능(
keywordSearch)을 활용했습니다. 프로젝트에서 사용하는 카카오맵 기반 지도 기능과의 연동성도 뛰어나고, 별도의 데이터 정합성 처리 없이도 호환성을 확보할 수 있었습니다. 사용자 입력에 대해서debounce적용하여 카카오 로컬 API에 비동기 요청을 전송했습니다. 응답받은 주소 데이터를 파싱하여 리스트 형태로 검색창 하단에 표시하고, 사용자가 특정 항목을 선택하면 해당 주소를 검색창에 자동 입력되도록 구현했습니다. 이를 통해 주소 입력의 정확도와 속도를 동시에 개선했습니다.Beta Was this translation helpful? Give feedback.
All reactions