← /projects • shipped 2025 Frontend Engineer · Solo frontend Next.js · Stripe · Directus

Awarelab Membership

투자 포트폴리오 분석 멤버십 플랫폼.

WHY I STARTED THIS
처음부터 결제까지 도는 멤버십 제품을 한 분기 안에 출시해야 했고, 프론트엔드를 맡을 사람은 나 한 명이었습니다. 마케팅 사이트·어드민·CMS 에디터·다국어를 한 사람이 일관된 아키텍처로 설계하면 더 빠르고 흔들림이 없겠다고 판단해 단독으로 맡았습니다.
LIGHTHOUSE
52 87
+35
Performance score
LCP
9.0s 3.8s
−44%
Largest Contentful Paint
BUNDLE
100% 54%
−46%
Initial JS payload
CLS
0.126 0
−100%
Layout shift

"왜 메인에서 LCP가 9초가 나올까?"

DEF. 원인을 추정하지 않고 진단부터: @next/bundle-analyzer로 본 결과, 첫 번들에 CMS와 차트 라이브러리가 통째로 들어가 있었습니다. 동시에 포트폴리오 탭/필터를 빠르게 전환하면 늦게 도착한 응답이 최신 화면을 덮어쓰는 race condition도 재현되었습니다.

PRODUCT
Investment portfolio analysis · subscription
TIMELINE
2025.04 → 2026.03 (11 months)
TEAM
5 total · 4 dev · 1 frontend (me)
SCOPE
Marketing + Admin + CMS editor + i18n
STACK
Next.js · TypeScript · Stripe · Directus
I OWNED
Architecture · perf · payments · deploy
01

FSD architecture from day one

기능이 늘어나도 폴더가 무너지지 않도록 Feature-Sliced Design을 채택. 5계층으로 의존성을 단방향으로 강제.

02

In-memory cache hook for race-prone fetches

탭/필터 전환 race condition을 closure + Map + Symbol 토큰 기반 캐싱 훅으로 해결. 늦은 응답은 자동으로 무시.

03

Bundle 46% diet

bundle-analyzer로 진단 → 미사용 패키지 동적 임포트, 폰트 서브셋, GSAP context().revert() 패턴으로 SPA 메모리 누수 차단.

04

Generic Zustand factory for filters

Admin에서 반복되는 필터 store를 제네릭 팩토리로 추상화. 도메인은 타입 인자로만.

05

Stripe flow as a state machine

결제 진입부터 완료까지의 상태를 단계별로 모델링. 취소·실패·중단·중복 경로를 명시적으로 처리.

The fetch that ignores stale answers.

키별로 in-flight 요청을 closure에 저장. 같은 키로 새 요청이 들어오면 이전 요청은 폐기 표시 — 늦게 도착해도 setState를 무시합니다.

function useRequestCache<T>(fetcher: (key: string) => Promise<T>) {
  const cache = useRef(new Map<string, T>());
  const inflight = useRef(new Map<string, symbol>());
  const [state, setState] = useState<{key?: string; data?: T}>({});

  const get = useCallback(async (key: string) => {
    if (cache.current.has(key)) {
      setState({ key, data: cache.current.get(key) });
      return;
    }
    const token = Symbol(key);
    inflight.current.set(key, token);
    const data = await fetcher(key);
    if (inflight.current.get(key) !== token) return; // stale
    cache.current.set(key, data);
    setState({ key, data });
  }, [fetcher]);

  return [state, get] as const;
}

Shipped subscription. Halved bundle. Killed race conditions.

  • Lighthouse 52 → 87, LCP 9.0s → 3.8s.
  • Bundle −46%, CLS 0.126 → 0.
  • Membership Stripe flow live in production.
  • Vitest + MSW 기반 API 독립 테스트 환경 구축.
  • i18n EN/KO/JA + SEO, Admin + TipTap CMS (12 custom extensions).