패치 상세
v1.0.1 📌 첫 가설을 기각하고 리전 불일치에서 홈 성능 병목 찾기
첫 가설을 재측정으로 기각한 뒤 Vercel Function과 Supabase의 리전 불일치를 발견해, 통제된 환경의 LCP 중앙값을 1.99초에서 0.83초로 약 58% 단축했습니다.
2026-08-11v1.0.1#PERFORMANCE#LCP#VERCEL#SUPABASE#OBSERVABILITY
변경 로그

핵심 성과

Vercel Runtime Logs로 Function과 Supabase의 리전 불일치를 진단하고 두 서비스를 서울 리전에 배치했다. 동일한 조건으로 각각 5회 측정한 중앙값 기준, 홈 LCP를 1.988초에서 0.828초로 약 58.4%, 문서 응답 지속시간을 1,374.2ms에서 294.8ms로 약 78.5% 단축했다.

| 측정 지표 | 개선 전 중앙값 | 개선 후 중앙값 | 변화 | | --- | ---: | ---: | ---: | | 홈 LCP | 1.988초 | 0.828초 | 약 58.4% 단축 | | 문서 응답 지속시간 | 1,374.2ms | 294.8ms | 약 78.5% 단축 |

여기서 문서 응답 지속시간은 PerformanceNavigationTiming.responseEnd - responseStart로 계산한 브라우저 관점의 응답 스트림 수신 구간이다. 순수 서버 실행시간이나 React 렌더링 시간으로 해석하지 않았다.

캡처 추가 위치: 개선 전후 Chrome Performance의 LCP 타이밍 비교 화면

1. 문제: 데이터가 필요한 메인 카드가 늦게 보였다

돈이담 홈에 진입하면 사용자별 메인 대시보드 카드와 최근 거래 내역이 늦게 표시됐다. Chrome Performance에서 LCP 요소를 추적하니 이미지나 정적 헤더가 아니라 DashboardCard.tsx의 메인 카드 문구가 대부분의 측정에서 LCP를 결정하고 있었다.

즉, 브라우저의 단순한 그리기보다 서버 데이터가 포함된 문서가 도착하기까지의 경로를 먼저 살펴봐야 했다.

캡처 추가 위치: Chrome Performance에서 p.text-xl.font-bold.text-primary-foreground.mt-1이 LCP 요소로 선택된 화면

2. 측정: 재현 가능한 조건부터 고정했다

개선 전후 결과를 비교할 수 있도록 다음 조건을 동일하게 적용했다.

  • 대상: donidam.today 운영 배포
  • 환경: Chrome Pixel 9 모바일 에뮬레이션
  • 네트워크: Fast 4G
  • CPU: 4× slowdown
  • 캐시: Disable cache
  • 서비스 워커: Bypass for network
  • 데이터: 동일 계정, 동일 데이터
  • 반복 횟수: 개선 전후 각각 5회
  • 대표값: 이상치를 임의로 제외하지 않은 중앙값

Serwist 서비스 워커의 캐시가 결과에 개입하지 않도록 측정 중에만 네트워크 우회를 적용했다. 이 결과는 통제된 합성 테스트이므로 실제 사용자 전체의 p75 Core Web Vitals로 일반화하지 않았다.

3. 첫 가설: 서버 컴포넌트의 내부 API 재호출

처음에는 홈 서버 컴포넌트가 같은 애플리케이션의 API Route를 HTTP로 다시 호출하는 구조를 병목으로 보았다.

사용자
  → 홈 서버 컴포넌트
  → 메인 카드 API + 최근 거래 API
  → API마다 Supabase 사용자 인증
  → Supabase 조회
  → 홈 서버 컴포넌트
  → 사용자

구조적으로 중복된 경로를 제거하기 위해 다음과 같이 개선했다.

  • page.tsx에서 Supabase 사용자 인증을 한 번만 수행
  • 인증된 user.id와 Supabase 클라이언트를 두 조회 함수가 공유
  • 메인 카드와 최근 거래 조회를 Promise.all()로 병렬 실행
  • 메인 카드 내부의 프로필과 월 지출 합계도 병렬 조회
  • 서버 조회 결과를 React Query 캐시에 setQueryData()로 주입
  • 기존 API Route도 동일한 공통 서버 함수를 사용하도록 정리

관련 커밋은 9de78ac perf: 홈 대시보드 서버 요청 최적화이다.

4. 가설 기각: 구조는 나아졌지만 속도는 나아지지 않았다

구조 개선 후 다시 측정했지만 문서 응답 지속시간 중앙값은 1,505ms, LCP 중앙값은 2,032ms였다. 최초 기준값인 1,374.2ms와 1,988ms보다 유의미하게 좋아지지 않았다.

내부 API 호출 제거는 인증 중복을 줄이고 데이터 접근 구조를 단순화했지만, 사용자가 체감한 지연의 주된 원인은 아니었다. 따라서 첫 가설을 기각하고 코드 바깥의 실제 실행 경로를 추가로 관측했다.

이 단계에서 얻은 핵심은 구조적으로 그럴듯한 개선과 실제 성능 개선을 동일시하지 않고, 재측정 결과로 가설을 판단한 것이다.

5. 실제 원인: 서울에서 받은 요청이 미국에서 실행되고 있었다

Vercel Runtime Logs에서 홈 / 요청을 확인한 결과는 다음과 같았다.

  • 요청 접수: 서울 icn1
  • Function 실행: 미국 워싱턴 D.C. iad1
  • Function Execution Duration: 약 1.54초
  • Response finished: 약 2초
  • 외부 요청: Supabase Auth 1회, profiles 1회, transactions 2회

Received in Seoul은 요청을 처음 받은 Vercel 네트워크 위치이고, Routed to iad1은 서버 컴포넌트가 실제로 실행된 위치다. 두 로그를 같은 의미로 보지 않는 것이 중요했다.

Supabase는 AWS 서울 ap-northeast-2에 있었기 때문에 인증과 데이터 조회 과정에서 미국과 한국 사이의 장거리 왕복이 반복됐다. 특히 사용자 인증이 끝나야 사용자 데이터 조회를 시작할 수 있어, 인증 단계와 조회 단계의 지연이 완전히 겹쳐지지 않았다.

캡처 추가 위치: Received in Seoul (icn1)Routed to Washington, D.C. (iad1)이 함께 보이는 Vercel Runtime Logs

개선 전 요청 흐름

한국 사용자
  → 서울 Vercel 네트워크 (icn1)
  → 미국 Vercel Function (iad1)
  ↔ 서울 Supabase Auth/DB (ap-northeast-2)
  → 한국 사용자

6. 해결: 컴퓨팅과 데이터 소스의 리전을 정렬했다

Supabase 호출이 많은 서버 함수를 데이터 소스와 같은 서울 리전에서 실행하도록 vercel.json을 설정했다.

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "regions": ["icn1"]
}

관련 커밋은 9823c36 perf: Vercel 함수 리전을 서울로 설정이다. 이 선택의 핵심은 단순히 사용자에게 가까운 서버를 고른 것이 아니라 컴퓨팅과 데이터 소스를 코로케이션해 서버와 DB 사이의 왕복 거리를 줄인 것이다.

개선 후 요청 흐름

한국 사용자
  → 서울 Vercel 네트워크 (icn1)
  → 서울 Vercel Function (icn1)
  ↔ 서울 Supabase Auth/DB (ap-northeast-2)
  → 한국 사용자

7. 결과: 동일 조건에서 중앙값으로 비교했다

리전 변경 후 문서 응답 지속시간은 294.8 / 241.5 / 240.0 / 322.7 / 1,175.6ms, LCP는 828 / 740 / 744 / 832 / 1,756ms로 측정됐다. 큰 값이 나온 5회차도 임의로 제외하지 않고 전체 5회의 중앙값을 사용했다.

5회차 편차는 콜드 스타트나 새로운 함수 인스턴스 실행 가능성을 생각해볼 수 있지만, 로그만으로 확정할 수 없어 원인으로 단정하지 않았다.

LCP 개선율
(1,988 - 828) / 1,988 × 100 = 약 58.4%

문서 응답 지속시간 개선율
(1,374.2 - 294.8) / 1,374.2 × 100 = 약 78.5%

캡처 추가 위치: 리전 변경 후 Vercel Runtime Logs에서 Function이 icn1에 실행된 화면

캡처 추가 위치: 개선 전후 5회 측정값과 중앙값을 정리한 표 또는 스프레드시트

사용 기술과 관측 도구

  • Chrome Performance: LCP 요소와 Navigation Timing 확인
  • Vercel Runtime Logs: 요청 접수 위치, Function 실행 위치, 외부 요청 관측
  • Supabase: Auth와 PostgreSQL 데이터 소스
  • Next.js Server Components: 홈 문서와 사용자별 대시보드 데이터 구성
  • TanStack Query: 서버 조회 결과를 클라이언트 캐시에 연결
  • Serwist: 측정 중 서비스 워커 캐시 개입 여부 통제

회고

첫 가설은 코드 구조만 보면 타당했지만 측정 결과로는 체감 성능의 원인을 설명하지 못했다. 개선을 완료했다고 가정하지 않고 같은 조건으로 다시 측정했기 때문에 가설을 기각할 수 있었고, Runtime Logs까지 관측 범위를 넓혀 애플리케이션 코드 밖의 배포 토폴로지에서 실제 병목을 찾았다.

이번 개선을 통해 성능 문제는 렌더링 코드만이 아니라 사용자, 컴퓨팅, 데이터 소스가 실제로 어느 리전에 배치되어 어떤 순서로 통신하는지까지 포함해 진단해야 한다는 점을 배웠다.