Frontend Lead
2026.01 — 2026.07
B2B SaaS 스타트업
제품 흐름과 프론트엔드 아키텍처를 설계하고, 인증·온보딩·결제 경험을 구현했습니다.
PORTFOLIO · SEOUL

정량 지표로 병목을 진단하고 개선 효과를 수치로 검증하며, 성능 문제를 구조적으로 해결합니다. 변경 영향 범위를 예측할 수 있는 코드 아키텍처를 설계해, 빠른 개선이 안정적으로 쌓이는 환경을 만듭니다.
EXPERIENCE
2026.01 — 2026.07
B2B SaaS 스타트업
제품 흐름과 프론트엔드 아키텍처를 설계하고, 인증·온보딩·결제 경험을 구현했습니다.
PROJECTS
공개 가능한 개인 프로젝트에서, 성능과 데이터 흐름을 어떻게 관찰하고 선택했는지 정리했습니다.
Velog에서 부족했던 해시태그 AND 필터·검색을 직접 구현해, 내용이 기억나지 않을 때 해시태그로 빠르게 재탐색할 수 있는 위키형 개인 기술 블로그입니다.
CASE FOCUS
읽기 비중이 극단적으로 높은 블로그에서, 동일 게시글 재방문 시에도 매번 DB를 조회하고 hydration 이후에도 클라이언트 중복 fetch가 발생하는 구조였습니다. mutation 이벤트 기반 태그 무효화와 클라이언트 hydration 계약 통일이라는 두 축으로 재정렬해, 캐시 히트 시 DB 쿼리 0건·백그라운드 불필요 요청 0건을 달성했습니다.
블로그는 읽기 비중이 극단적으로 높고 콘텐츠 변경이 드문 서비스입니다. 그러나 서버 캐시 계층 없이 구성하면 동일 게시글을 반복 방문할 때도 요청마다 Supabase DB에 직접 접근하는 구조가 됩니다.
게시글 상세 페이지는 동일 `postId`에 대한 읽기가 반복되더라도 매번 DB를 조회했고, 글 목록과 검색은 서버에서 내려주는 초기 데이터와 클라이언트 `queryKey`가 일치하지 않아 hydration 이후에도 중복 fetch가 발생하는 구조였습니다. 또한 갱신 빈도가 낮은 목록에 주기적 polling이 설정되어 있어 불필요한 백그라운드 요청이 지속되었습니다.
서버 캐시 계층의 부재와 서버-클라이언트 데이터 흐름의 불일치, 두 가지가 핵심이었습니다.
`revalidatePath`는 경로 단위로 무효화 범위가 넓어, 댓글 하나가 추가되어도 해당 게시글 페이지 전체가 재생성됩니다. 글 본문·댓글·해시태그·조회수가 독립적으로 무효화되어야 하는 데이터 구조에서는 범위를 제어할 수 없어 기각했습니다.
ISR(`revalidate: number`)은 주기적으로 콘텐츠가 갱신되는 서비스에 적합하지만, 이 블로그는 작성·수정이라는 mutation 이벤트 기반으로 갱신됩니다. TTL이 남아 있는 동안 mutation 직후에도 구버전 스냅샷이 독자에게 노출될 수 있어 정합성을 보장하기 어렵다고 판단했습니다.
Redis 같은 외부 캐시는 세밀한 제어가 가능하지만 추가 인프라 비용과 운영 복잡도가 따릅니다. 개인 블로그 규모에서는 과도한 설계라고 보았습니다.
`unstable_cache` + `revalidateTag` 조합을 선택했습니다. TanStack Query가 query factory 패턴으로 key 계층을 설계해 무효화 범위를 제어하듯, Data Cache에도 `posts`·`post-${id}`·`comments`·`hashtags` 태그 계층을 두어 mutation 종류에 따라 영향받는 캐시만 선택 무효화할 수 있습니다. Data Cache는 Next.js 서버가 관리하므로 별도 인프라 없이 요청 간 스냅샷을 공유할 수 있습니다. 다만 TTL 내 stale 가능성이 있으므로 `revalidateTag`를 주 전략으로, TTL은 보조 안전망으로 두기로 했습니다.
클라이언트 측 문제는 두 가지로 진단했습니다. `/search`가 `initialData` 방식을, `/posts`가 `prefetchInfiniteQuery` 방식을 써서 hydration 계약이 달랐고, 동일한 화면이라도 서버 시드가 클라이언트 TanStack 캐시로 이어지지 않아 중복 fetch가 발생했습니다. 또한 변경이 드문 글 목록에 주기적 polling이 설정되어 있어 실제 갱신과 무관한 반복 요청이 지속됐습니다. 두 문제 모두 mutation 이벤트 후 명시적으로 동기화하는 방식으로 해결할 수 있으므로, 클라이언트 패칭 계약을 단일 방식으로 통일하고 polling을 제거하기로 결정했습니다.
글·댓글·해시태그에 해당하는 모든 서버 읽기 함수를 `unstable_cache`로 감쌌습니다. 각 함수는 데이터 종류와 식별자를 기반으로 캐시 키를 구성하고, 무효화 태그를 지정해 mutation 시 영향받는 스코프만 갱신할 수 있도록 했습니다.
태그 문자열과 `unstable_cache` 키 배열의 대응을 `cache-tags.ts`에 상수로 중앙화했습니다. `revalidateTag` 호출과 `unstable_cache` 등록이 동일 상수를 참조하도록 해 무효화 누락이나 불일치 가능성을 차단했습니다.
좋아요 여부는 `userId + postId` 조합을 캐시 키에 포함해야 하므로 Data Cache에서 제외했습니다. Server Action으로 fresh 조회하고 클라이언트 TanStack Query와 낙관적 업데이트로 처리해 공용 글 스냅샷과의 캐시 경계를 분리했습니다.
`/posts`와 `/search`가 제각각이던 패칭 방식을 단일 계약으로 통일했습니다. 두 경로 모두 서버에서 `prefetchInfiniteQuery` → `dehydrate` → `HydrationBoundary`로 시드하고, 클라이언트 `useInfiniteQuery`가 동일 `queryKey`를 바라보도록 맞췄습니다. 글 목록에 설정되어 있던 주기적 polling은 제거하고, mutation 후 `revalidateTag`와 `invalidateQueries` 연결로 대체했습니다.
Supabase API 로그 기준 — 동일 게시글 재방문 시 두 번째 요청부터 DB 접근 없음 확인
posts·comments·hashtags로 분리. 예: 댓글 추가 시 comments 태그만 무효화, 글 본문 스냅샷 유지
목록 polling 제거 후 — mutation 직후 revalidateTag → invalidateQueries 연결로 대체
군 복무 중 700~800권 규모 도서에 대한 검색 시스템이 없고 수기·분산 관리로 자료와 실제 위치가 어긋나던 문제를 해결하기 위해 개발한 도서 위치 안내 서비스입니다.
CASE FOCUS
첫 방문 시 Slow 4G 기준 LCP가 4.69s에 달했고, 이미지가 브라우저 캐시에 올라있어도 모달에서 DB 응답이 올 때까지 렌더를 기다리는 구조였습니다. 이미지 전달 경로·요청 시점·데이터 확정 흐름 세 가지를 재정렬해 Cold 기준 LCP를 2.81s(40% 단축)로 줄이고, 모달 전환 직후 skeleton 없이 이미지가 즉시 표시되도록 했습니다.
사용자가 도서관 책을 검색한 뒤 위치를 확인하기까지의 흐름에서, 이미지 로딩이 세 지점에 걸쳐 사용자를 기다리게 만들었습니다.
메인 화면의 위치 지도 이미지는 페이지에서 가장 큰 영역을 차지하는 LCP 요소였지만, 첫 방문 시 Slow 4G 환경 기준으로 LCP가 4.69s까지 지연됐습니다.
검색 결과 카드에서 상세 페이지(모달)로 넘어가면, 전환이 이루어진 뒤에야 지도 이미지 다운로드가 본격화됐습니다. 이미 상세 화면 안에 있는데도 이미지가 뒤늦게 붙어 LCP 마커가 약 5.8s까지 늦게 찍혔습니다.
모달에서는 이미지가 브라우저 캐시에 있더라도 location 번호를 DB에서 받아야 렌더가 시작되는 구조였습니다. `useEffect` → Server Action → DB 조회 → location 확정 → `<Image>` 렌더의 순차 워터폴이 preload 효과를 상쇄하며 skeleton이 노출됐습니다.
세 문제 모두 이미지 전송 크기, 이미지 요청 시점, 렌더에 필요한 데이터 확정 시점이 각각 사용자의 전환 흐름보다 늦게 정렬된 데서 비롯되었습니다.
Chrome DevTools Performance·Network·Lighthouse를 병행해 병목을 측정했습니다. JPEG 기준 약 76KB 이미지에 다운로드만 약 2.45s가 소요됐고, LCP 후보 이미지의 Resource Load Delay도 함께 관찰됐습니다. Warm 캐시 상태에서는 체감이 개선됐지만 Cold Start에서는 동일한 지연이 반복됐습니다.
이미지 최적화 방법으로 Next.js 내장 Image Optimization, Cloudflare Images, ImageKit을 검토했습니다.
Next.js 내장 방식은 서버 측 변환이라 트래픽 증가 시 서버 부하·비용이 함께 증가합니다. 또한 위치 지도 이미지의 컨테이너 폭이 뷰포트에 따라 유동하는 구조라, 정적 `deviceSizes` breakpoint 기반 srcset + `sizes` 힌트 방식으로는 렌더 시점의 실제 폭에 정확히 맞는 크기를 요청하기 어려워 과다 전송이 발생할 수 있었습니다. Cloudflare Images는 Supabase Storage와의 연동에 별도 파이프라인 구성이 필요해 현재 스택 대비 복잡도 증가가 컸습니다.
ImageKit은 커스텀 로더에서 렌더 시점의 실제 폭(`w-{actual_width}`)을 URL 파라미터로 주입할 수 있어 동적 컨테이너에서도 정확한 크기를 요청할 수 있고, URL 파라미터 기반 실시간 변환과 글로벌 Edge 캐싱, Supabase Origin 연동 단순함을 함께 제공합니다. ImageKit으로 결정했습니다.
이미지 경로가 CDN으로 바뀐 뒤에도 검색 카드→모달 전환 시 LCP 타임라인이 늦게 찍히는 문제가 남았습니다. 사용자가 카드를 클릭하기 전, 포인터가 링크 위에 올라오거나 탭하기 직전에 LCP 후보 이미지 요청을 시작해 전환 후 다운로드 대기 시간을 줄이는 방향으로 결정했습니다.
DB 워터폴은 `ToLocationButton`이 이미 `location` prop을 보유하고 있다는 점에서 해법을 찾았습니다. 해당 값을 URL 쿼리 파라미터(`?loc=N`)로 모달에 전달하면, 모달 마운트 시점에 `useSearchParams()`로 동기적으로 location을 읽을 수 있어 Server Action 호출이 불필요해집니다. URL에 직접 접근하는 경우에는 기존 DB 조회 폴백을 유지하기로 결정했습니다.
CDN 도입으로 외부 서비스 의존성이 추가되지만, 서버 부하 분산과 Edge 캐싱 효과로 Cold Start 편차를 구조적으로 줄일 수 있다고 판단했습니다.
위치 지도처럼 LCP에 직접 영향을 주는 대용량 이미지는 Supabase Storage에서 ImageKit Origin으로 경로를 옮겼습니다. 책 표지 등 상대적으로 가벼운 이미지는 기존 Supabase URL을 유지했습니다.
ImageKit 경로에는 next/image 커스텀 로더를 연결해 모든 요청에 `f-auto·w-{width}·q-{quality}` 변환 파라미터를 일관 적용했습니다. 페이지 유형에 따라 LCP 후보가 달라지므로 `priority`를 분기 적용했습니다. 풀 페이지(`/location/[id]`)에서는 상단 표지에, 인터셉트 모달에서는 화면 대부분을 차지하는 지도에 적용했습니다.
`ToLocationButton`의 `onMouseEnter`와 `onPointerDown`에 동일한 선요청 핸들러를 연결해, 사용자가 아직 상세 화면에 도달하기 전에 지도 이미지 요청을 시작했습니다. 선요청 URL은 `getLocationMapPreloadUrl`로 생성해 실제 렌더에 쓰이는 변환 파라미터와 동일하게 맞췄습니다.
`ToLocationButton`의 href에 `?loc=N`을 추가해 모달이 마운트 직후 `useSearchParams()`로 location을 동기적으로 읽도록 변경했습니다. 이로써 `useEffect` 실행 없이 이미지 렌더가 시작됩니다. `loc` 파라미터 없이 URL에 직접 접근하는 경우에는 기존 DB 조회 폴백이 실행됩니다.
약 66% 감소
약 35% 단축
Lighthouse Cold 기준 반복 측정 — 캐시 미적재 상태 기준
약 40% 단축
약 25% 단축
soft navigation 경로에서 Server Action 호출 완전 제거
CAPABILITIES
인터페이스, 상태, 검증, 배포까지 제품을 완성하는 데 사용하는 기술입니다.
TypeScript · JavaScript
React · Next.js (App Router) · HTML5 · CSS3 · Radix UI · shadcn/ui
Tailwind CSS · Styled Components
TanStack Query (React Query) · Zustand · Redux Toolkit · nuqs
React Hook Form · Zod
Vitest · React Testing Library (RTL) · Playwright · MSW
Supabase (PostgreSQL/Auth/Storage) · MySQL
ImageKit (CDN/이미지 변환)
Firebase Cloud Messaging
Git · GitHub · Vercel · AWS S3 · ESLint 9 · Prettier · Husky · lint-staged · pnpm
Jira · Figma · Notion
Claude Code · Codex · Cursor AI
WRITING
문제를 다시 만나도 재현할 수 있도록 기술적 선택과 맥락을 글로 정리합니다.
Axios 인터셉터와 세션 검증 훅에서 동시에 발생하던 JWT refresh 이중 호출 Race Condition을 Singleton Promise로 단일화해, 토큰 갱신과 리다이렉트 과정의 불안정성을 제거했습니다.
Next.js 환경에서 CORS 에러가 왜 발생하는지 Same-Origin Policy 관점으로 정리하고, Route Handler 프록시 등 해결 전략을 구조적으로 설명합니다.
다중 선택 토글 순서에 따라 TanStack Query queryKey가 달라져 캐시 분산/재요청이 늘어나는 문제를, 스토어 레벨에서 ID 집합을 정렬해 키를 안정화했습니다.
WAI-ARIA 역할과 키보드 핸들러로 접근성을 보강하고, mousemove에 rAF 기반 렌더 스케줄링을 적용해 사이드바 리사이즈 jank를 줄였습니다.
EDUCATION
식품생명공학과 졸업
교양 수업에서 프로그래밍을 처음 접한 후, 소프트웨어로 문제를 해결하고 서비스를 직접 배포할 수 있다는 점에 매력을 느껴 졸업 전 개발자로 전향을 결심했습니다.