← portfolio

Lettie

2025 · Next.js · TypeScript · Tanstack Query · Vanilla Extract · Playwright

타임캡슐 웹 서비스

여러 명이 함께 편지를 담을 수 있는 타임캡슐을 만들고, 링크로 친구들에게 공유해 편지를 모은 뒤, 설정한 날짜가 되면 이메일로 캡슐 오픈 알림을 받아 모두의 편지를 함께 열어 보는 감성 기반 웹 서비스입니다.

사용 기술 Next.js, TypeScript, Ky, TanStack Query, React Hook Form Vanilla-extract, Storybook Motion, Matter.js, Playwright

프로젝트 설계, 초기 세팅, 핵심 기능 개발 전반을 주도하며, 많은 사용자의 흥미와 참여를 끌어낼 수 있는 서비스를 만드는 데 집중했습니다. 사용자 플로우를 직접 설계해 반복적으로 다듬었고, 반응형 대응과 다양한 브라우저 환경까지 함께 고려해 어디서든 같은 경험이 유지되도록 했습니다. 공통 컴포넌트디자인 토큰을 설계하고, screen 토큰 유틸리티와 모션 추상화로 반응형·인터랙션 코드를 일관되게 관리할 수 있도록 정리했습니다. Storybook 기반 UI 테스트 자동화, CI/CD 파이프라인, lefthook 기반 개발 규칙 정비까지 함께 진행해 개발 경험과 코드 품질을 함께 끌어올렸습니다.

쿼리 키 팩토리 패턴과 queryOptions로 쿼리 레이어 통합

문제 상황 쿼리 키를 문자열 그대로 흩뿌려 쓰다 보니 오타와 중복이 잦았고, 어떤 키가 어디서 쓰이는지 추적이 어려워 invalidation 시 키가 누락되며 캐시가 어긋나는 버그가 반복됐습니다. 도메인별 쿼리 키·옵션·API 호출 함수도 흩어져 있어, 같은 쿼리를 여러 화면에서 재사용하기도 번거로웠습니다. 해결 query-key-factory 패턴을 차용해 쿼리 키를 한 곳에서 계층적으로 정의했습니다. capsuleQueryKeys.detail(id)처럼 함수 호출로만 키를 만들게 해 오타를 원천 차단하고, params 타입을 팩토리에 묶어 호출부에서 자동완성과 타입 안전성을 함께 확보했습니다. detail(id)all()을 펼쳐 만들어지는 계층 구조라, 상위 키(all()) 하나만 무효화해도 하위 쿼리까지 prefix 매칭으로 함께 invalidation되도록 잡았습니다. 여기에 TanStack Query의 queryOptions를 더해 쿼리 키·queryFn·옵션(enabled 등)을 하나의 재사용 가능한 옵션 객체로 콜로케이션했습니다. 호출부는 useQuery(capsuleQueryOptions.capsuleDetail(id))처럼 옵션을 넘기기만 하면 돼, 도메인마다 커스텀 쿼리 훅을 따로 만들 필요가 없어졌습니다.

export const capsuleQueryKeys = {
  all: () => ["capsule"],
  detail: (id: string) => [...capsuleQueryKeys.all(), id],
};

export const capsuleQueryOptions = {
  capsuleDetail: (id: string) =>
    queryOptions({
      queryKey: capsuleQueryKeys.detail(id),
      queryFn: () => getCapsuleDetail(id),
      enabled: !!id,
    }),
};

구조 설계 쿼리는 재사용성 중심이라 shared/api/queries 아래 도메인(capsule·letter 등)당 한 파일에 쿼리 키·옵션·API 함수를 함께 두고, 뮤테이션은 맥락·행위 기반이라 shared/api/mutations 아래 훅 단위로 파일을 나눴습니다. 쿼리와 뮤테이션의 성격 차이에 맞춰 폴더 구조를 의도적으로 비대칭으로 설계했습니다. 성과 새 쿼리를 추가할 때 별도 훅 없이 옵션 정의만으로 끝낼 수 있게 됐고, 타입 안전한 키 덕분에 키 누락으로 인한 invalidation 버그가 사라졌습니다. 같은 옵션을 여러 화면에서 그대로 재사용할 수 있어 데이터 패칭 코드의 일관성도 함께 확보했습니다.

App Router 기반 모달 라우팅 아키텍처 개선

문제 상황 편지쓰기 모달이 URL과 독립적으로 동작하던 구조라, 모달을 연 채로 새로고침하거나 뒤로가기를 누르면 모달만 사라지거나 의도치 않은 화면으로 빠지는 문제가 있었습니다. 공유 가능한 URL도 따로 없어, 특정 작성 화면을 그대로 전달하기도 어려웠습니다. 해결 Next.js App Router의 Intercepting RouteParallel Route를 적용해 모달 상태를 URL 기반으로 관리하도록 구조를 바꿨습니다. 페이지 안에서 진입할 때는 모달로, 새로고침이나 외부 URL로 직접 접근할 때는 전체 페이지로 자연스럽게 분기되도록 라우팅을 분리했습니다. 성과 모달 내용을 URL로 공유할 수 있게 됐고, 같은 URL이라도 인앱 진입은 모달, 새로고침·직접 접근은 전체 페이지로 자연스럽게 분기됩니다. 새로고침 시 모달이 그냥 사라지지 않고 전체 페이지로 컨텍스트가 유지되며, 뒤로가기에서는 모달이 깔끔하게 닫히고 앞으로 가기에서 다시 열리는 일관된 흐름을 만들었습니다.