[경기문화재단] 아트경기 기술스택 마이그레이션
역할 풀스택 전담: 프론트엔드, CMS, DB, 인프라 전체 (1인 / 경기문화재단 외주 / 안전관리실 및 예술지원팀과 협업)
요약 쿼리 개선, 측정, 아키텍처 재설계 순으로 사이트 성능 문제를 해결함. Lighthouse 성능 점수 68 → 94, 서버 3대 → 1대(운영비 -71%).
프로젝트 개요
아트경기 사이트에서 사이트 로딩이 현저히 느려지는 성능 문제 발생
성능 개선 작업을 요구하여 아키텍처 단의 최적화와 전체 페이지 코드 재작성
문제 상황: 렌더링 전략이 DB 쿼리까지 침범
2025년, 아카이빙된 작가 정보 300명이 업로드되자 작가 페이지의 속도가 크게 저하되는 문제가 발생했습니다.



기존의 구현이 “모든 작가(상세 포함) 데이터를 한 번에 가져온 후 클라이언트 라우트”하는 식으로 되어 있었습니다.
최초 접근이 상세 페이지인 경우에도 모든 작가 데이터를 로드하게 됩니다.
2025년에 기존 아카이브된 작가들이 모두 업로드되었고, 업로드된 모든 작가 데이터의 용량은 3.07MB (gzip 454KB) 였습니다.
이 프론트엔드 설계는 DB 요청에까지 전이되어 있었습니다.
strapi.find('authors', {
pageSize: 1000, // 모든 작가
populate: {
featured_works: { populate: 'media' }, // 작가마다 작품·이미지 전체
projects: ...
}
})이 요청은 실제 Postgres에서 살펴봤을 때 아래와 같이 N+1 쿼리가 확인됐습니다.
작가 전체 → 컴포넌트 링크 → 미디어 테이블 조인 → 파일 테이블로 이어지는 깊은 관계를, 모든 행에서 SELECT *로 스캔합니다.
SELECT * FROM authors WHERE published_at IS NOT NULL ORDER BY name LIMIT 1000;
SELECT * FROM authors_components WHERE author_id IN (...전체...) AND field='featured_works';
SELECT * FROM components_author_artworks WHERE id IN (...작가×작품...);
SELECT * FROM files_related_morphs WHERE related_type='components_author_artworks' AND ...; -- 가장 비싼 단계
SELECT * FROM files WHERE id IN (...모든 작품 이미지...);해결 과정
1. 쿼리 최적화: 리스트용 쿼리와 상세 페이지용 쿼리 분리
리스트에는 리스트용 축소 데이터만, 상세 페이지는 개별 요청으로 분리했습니다.


| 지표 | 개선 전 | 쿼리 개선 |
|---|---|---|
| 성능 점수 | 68 | 74 |
| FCP(초) | 2.0 | 1.9 |
| LCP(초) | 3.1 | 2.8 |
| Speed Index(초) | 3.2 | 1.9 |
| TTFB(ms) | 670 | 46 |
TTFB는 해결됐지만 FCP, LCP가 거의 움직이지 않았습니다. 남은 병목이 쿼리가 아니라 렌더 계층에 있다는 것을 수치로 확인했습니다.
2. 2개의 SvelteKit 및 Strapi CMS를 1개의 Next.js에 통합
CMS를 교체하는 것을 검토하던 중 별다른 비용이 들지 않고, 자체 호스팅이 되는 Payload CMS를 접했습니다. (Payload CMS는 Next.js 기반입니다). Payload CMS를 먼저 도입했을 때 편집 및 컨텐츠 추가 등의 속도는 현저히 빨라졌지만 사용자에게 보여지는 성능 지표는 개선되지 않았습니다.

기존 구조는 SvelteKit 앱 2기(데스크탑/모바일)와 Strapi CMS가 별도 프로세스로 운영되어, 캐시 미스나 SSR 시 프론트 서버에서 CMS로 추가 HTTP 요청이 발생했습니다.
여기에 Strapi CMS 자체의 CRUD 속도가 좋지 못해 운영 측에서 불편을 호소하였습니다.
이에 다음 근거로 Next.js 단일 앱(Payload CMS 내장)으로의 마이그레이션을 결정했습니다:
Payload CMS의 우수한 성능: 현저히 빠른 CRUD 속도
Payload CMS의 구조상 이점:
프론트엔드 - CMS - DB라는 불필요한 HTTP 통신 Hop이 사라지고, 타입 및 스키마도 자동 관리됩니다.두 앱이 비즈니스 로직과 데이터 모델을 공유하고 표현 계층만 상이: 반응형 단일 앱으로 통합하는 편이 중복 코드와 배포, 검증 비용을 줄이는 데 유리했습니다.
2-2. Why Payload CMS?
자체 호스팅을 지원하면서도 Next.js와 긴밀하게 통합됩니다. 다른 오픈소스 CMS에 비해 매우 빠른 admin 페이지 경험을 할 수 있었습니다. 무엇보다 따로 ‘admin을 관리’하는 상황이 없어 개발자 입장에서도, 컨텐츠 편집자 입장에서도 학습이 필요가 없었습니다.
2-3. 부가적인 개선
tailwind CSS와Pure CSS가 혼재되어 있던 기존 코드를Vanilla Extract로 모두 통합했습니다.tailwind CSS는 강력한 도구이지만 학습곡선이 필요합니다.
Vanilla Extract는 강제로 CSS를 파일 단위로 분리시켜 컨벤션을 유지하고, 강력한 타입 지원이 가능해 채택했습니다.
Payload CMS까지 Next.js 앱에 통합되면서3개 앱 + shared module의 모노레포 구조였던 것이 단일 앱으로 통합되면서 프로젝트 전체의 구조가 간결해졌습니다.
성과
1. 성능 지표의 비약적 향상
ISR를 적용하면 프리렌더된 HTML을 캐시에서 바로 서빙 및 갱신하게 되고, 아래와 같은 결과를 측정할 수 있었습니다:

| 지표 | 개선 전 | 쿼리만 개선 | Next.js (ISR) |
|---|---|---|---|
| 성능 점수 | 68 | 74 | 94 |
| FCP(초) | 2.0 | 1.9 | 0.7 |
| LCP(초) | 3.1 | 2.8 | 1.6 |
| Speed Index(초) | 3.2 | 1.9 | 0.7 |
| TTFB(ms) | 670 | 46 | 24 |
이는 Sveltekit에서도 리버스 프록시 단에서 Cache-Control header를 통해 비슷한 동작을 구현할 수 있습니다.
다만 Next.js의 경우 프레임워크 단에서 API를 제공해 캐시 관리가 네트워크가 아닌 데이터 조작 단에서 가능하다는 점과, 페이지 단위가 아닌 함수 단위로 (use cache) 캐시 주기를 쪼갤 수 있다는 점에서 이점이 있다고 생각합니다.
2. 운영 비용 절감
기존 아키텍처 구조를 간소화하여 운영비를 절감했습니다:
인스턴스
4GB x2 + 8GB x1→4GB x1월 270,000원 → 월 78,000원으로
71.1%의 인스턴스 운영비 절감
모노레포 (3개 앱 + shared module) 구조도 단일 앱으로 단순해져 유지보수 복잡도 또한 크게 내려갔습니다.
회고
이 구조 문제는 2025년 인수 때부터 염두에 두던 것으로, 첫 해에는 인프라 안정화를 우선했고 올해 클라이언트의 성능 개선 요청과 함께 공수를 확보해 진행했습니다.
성능 문제의 최초 원인은 데이터 조회량처럼 보였지만, 실제로는 렌더링 전략, 배포 구조, CMS 통신 방식이 모두 연결된 문제였습니다. 쿼리 최적화보다 아키텍처를 현재 요구사항에 맞게 재설계한 작업입니다.
풀스택 프로젝트에서는 프론트엔드 렌더링 설계가 DB 쿼리까지 전이되지 않도록, 계층별 설계를 의식적으로 분리해야 한다는 원칙을 얻었습니다.