
1. 도입전에 고민했던 점
고민보다 GO
사실 고민이 정말 많았다. 여러가지 이유가 있었는데 일단 첫 번째로 시간적인 여유가 부족했다. 예상했던 시간보다 작업이 지연되면서 내가 Next 코드를 만질만한 시간적 여유가 없어졌다. 보통 팀플 역할을 기획, 개발, 인프라를 나누어 하는게 맞다고 생각하긴 하는데, 이번 카테캠에선 테크 리더로서 뭔가 어깨가 무겁달까? 개인적으로 학습의 욕구가 커서 일을 벌리고 있는 것 같다.
둘째로 지금하고 있는건 팀 프로젝트인데, 나 혼자 마이그레이션하고 코드를 만지면 의미가 있을까? 하는 생각이 있었다. 카테캠 과정동안 멘토님들과의 멘토링, 각종 블로그들을 읽었는데, 팀에 엔지니어링이 필요한 문제가 있을때, 한명이라도 경험자가 있고 책임질 수 있다면 시행하는게 좋다고 들었다. 또, 부트 캠프에서 학습을 하는건 바람직하고, 각자가 이 프로젝트를 생각하는 우선순위도 다르니, 개인적인 공부도 하면서 팀의 프로젝트도 성장시키면 좋겠다는 생각을 했다.
처음에 FE 아키텍처를 설계할 때는 vite react는 그대로 정적 배포 해두고, 혼자 SSR서버를 운영할까? 이런 생각을 갖고 있었는데, 그럴바에는 Next.js로 한번에 처리하는게 맞다고 생각했다. FE팀의 결과물은 vite react 그대로 유지하면서 Next.js를 혼자 적절한 최적화를 해서 함께 두거나 새로운 저장소를 만들까도 생각중이다.
2. CDN의 힘을 버리고 Next.js를 도입한 이유
1. SPA의 한계를 극복해보자
Vite React + CloudFront 정적 배포 방식은 CSR로 동작하여 다음과 같은 한계가 있었다.
- 검색 엔진 크롤링: 초기 HTML에 콘텐츠가 없어 SEO에 불리
- 소셜 미디어 공유 문제: Open Graph 메타 태그가 동적으로 생성되지 않아 링크 미리 보기 불가
- 초기 로딩 성능: JavaScript 번들을 모두 다운로드한 후에야 콘텐츠 표시

사실 Nextjs를 도입하지 않고 vite만으로 충분히 위의 문제들을 해결할 수 있다. 위의 아키텍처가 그 예시인데 하나씩 살펴보면서 비교해보자.
- 검색 엔진 크롤링이 어렵다는게 무슨 뜻일까?
- SPA : SPA는 수백개의 라아트가 있어도 크롤러가 처음 받는 파일은 index.html 하나이다. 왜냐하면 index.html을 받아와서 JS가 클라이언트에서 모든걸 랜더링 하기 때문이다.
- 극복해보기 : Vite에서 Prerendering으로 빌드시점에 HTML을 미리 생성해서 사용자가 들어오기 전에 미리 html을 만들어둘 수 있다. 예를들면
/about페이지라면about.html이 추가적으로 생성되어 있어서 즉시 제공이 가능하다. 크롤러가 인식가능한 장점이 있지만, JS로드가 안되어 버튼같은 동적인 요소가 동작을 안하는 문제는 있다. - Nextjs : Nextjs는 SSR을 포함한 다양한 랜더링 전략을 지원한다. 서버에서 이미 완성된 HTML을 생성해 제공하므로, 검색 엔진은 JS 실행 없이도 완전한 페이지 내용을 읽을 수 있다.
- 메타 테그 생성의 어려움
- SPA : 위에서도 언급했지만, SPA는 깡통 index.html만 받아서 meta테그를 설정해 둘 수 없다. 작성…
- 극복해보기 :
if(userAgent == "bot")cloudfront function으로 요청이 만약 bot이라면 람다함수를 사용해 메타 테그를 주입해줄 수 있다. 메타 테그를 저장해두는 서버를 두고 람다와 상호작용하게 두면 훌륭한 아키텍처를 구축할 수 있다. - Nextjs : 라우트별로 메타 테그를 동적으로 생성 가능하기 때문에 검색엔진이 실제 메타 데이터를 읽을 수 있다.
2. FE 개발자로서의 엔지니어링 욕심
사실 위에 만들어둔 이미지대로 사용하는게 서버 운용 비용도 없고, CDN의 강력한 기능을 그대로 사용할 수 있어서 좋다. 하지만 개발자로서 FE 개발자로서 극복해보고 싶은 챌린지들이 있었고, 이번 카테캠 프로젝트를 통해 성장하고 싶었다.
- 이미지 최적화 (Next Image)
- lighthouse 지표 챌린지
- 컨테이너 기술에 대한 정복욕?
- 급증하는 트래픽에도 유연하게 대응하는 SSR 서버 운용
브라우저의 추상화가 점점 잘되고, 그 위에서 api로 노는 FE지만 … 성능을 최적화하고, 서버를 운용하면서도 비용 최적화에 대해 고민해보고 트래픽에도 유연하게 대응하는 등의 경험을 해보고 싶었다.
3. Next.js 단계적 마이그레이션 과정

1. FSD 아키텍처를 Next.js에 적용하기
FSD (Feature-Sliced Design) 아키텍처란?
FSD는 프론트엔드 프로젝트를
Layer,Slices,Segment로 구조화하는 아키텍처 방법론이다.- 단방향 의존성: 상위 레이어는 하위 레이어만 참조 가능
- 높은 응집도: 관련된 코드를 한 곳에 모음
- 낮은 결합도: 각 슬라이스는 독립적으로 동작
Next.js App Router와 FSD 통합
FSD 아키텍처는 정말 잘 설계된 아키텍처이지만,
app레이어를 사용할 때 문제가 생긴다. Next.js는 프레임워크 이기 때문에 정해진 형식에 따라 코드를 설계해야한다. FSD에서 최상위 계층인 app레이어가 Nextjs 13부터 라우팅 폴더로 사용되기 때문에 충돌이 생긴다.해결방법은 구글링 해보니 여러가지가 있다. 크게 나눠보면
app폴더와src폴더를 루트에 함께 두는 방식과src폴더 하나를 두고 사용하는 방식이 있는데, 나는 후자를 선택하고 아래 같은 폴더 구조를 구현했다.app폴더에 페이지는 그대로 네이밍하고, 페이지가 아닌provider,styles등의 페이지가 아닌 요소는_언더바를 붙여서 처리했다.
src/
├── app/ # Next.js App Router + (Routing Layer)
│ ├── (auth)/ # 페이지 그룹
│ │ ├── login/
│ │ ├── signup/
│ │ └── forgot-password/
│ ├── content/[id]/ # 콘텐츠 상세 페이지 (동적 라우팅)
│ ├── _providers/ # 전역 Provider
│ ├── layout.tsx # 전역 layout
│ ├── page.tsx # 홈페이지
│ └── ...
│ -> 기존과 동일하게 운용
├── entities/ # 비즈니스 엔티티 (Domain Layer)
├── features/ # 기능 단위 (Feature Layer)
└── shared/ # 공유 리소스 (Shared Layer)
2. 프레임워크 활용해보기
정적/동적 meta data 생성기
- 정적 meta data
// src/app/layout.tsx
export const metadata: Metadata = {
title: { default: 'K-SPOT', template: '%s | K-SPOT', },
description: 'K-콘텐츠 촬영지 기반 여행 계획 서비스',
keywords: ['K-드라마', '촬영지', '여행', '한국'],
openGraph: { type: 'website', locale: 'ko_KR', siteName: 'K-SPOT', },
};- 동적 meta data
// src/app/content/[id]/page.tsx
export async function generateMetadata({
params
}: ContentDetailPageProps): Promise<Metadata> {
const { id } = await params;
const content = await getContentDetail(id);
return {
title: content.title,
description: content.description,
openGraph: { title: content.title, description: content.description,
images: [{ url: content.posterImageUrl, width: 1200, height: 630, alt: content.title,}],
type: 'website',
},
twitter: { card: 'summary_large_image', title: content.title, description: content.description, images: [content.posterImageUrl],},};
}SEO 개선 결과
- Google 검색 노출: (주요 키워드)
- 소셜 미디어 공유: 링크 미리보기 정상 작동
- 크롤링 효과 개선
<Image/>컴포넌트 사용기
Next.js Image 컴포넌트는 아래와 같은 최적화 기능들을 제공해준다. k-contents를 다루는 우리팀에선 드라마,영화 처럼 포스터 이미지나 장소 이미지들을 사용하는 경우가 매우 많아서 이점을 활용하기로 했다.
- 포맷 변환: WebP, AVIF 등 최신 포맷 자동 제공
- 리사이징: 디바이스 크기에 맞는 이미지 자동 생성
- Lazy Loading: 뷰포트에 진입할 때만 로드
- Placeholder: 블러 효과로 CLS(Cumulative Layout Shift) 방지
// 1. 기본 사용 (고정 크기)
<Image
src="/hero-image.jpg"
alt="Hero"
width={800}
height={600}
priority // LCP 이미지는 priority 설정
/>
// 2. Fill 모드 (부모 크기에 맞춤)
<div className="relative w-full h-200">
<Image
...
/>
</div>
// 3. 반응형 이미지
<Image
...
/>성능 개선 결과
- 이미지 용량 감소 (JPEG → WebP)
- LCP 감소 (lighthouse 지표 개선)
서버 컴포넌트 활용기
- 데이터 패칭 최적화
// 서버 컴포넌트에서 병렬 데이터 페칭
export default async function ContentDetailPage({ params }) {
const { id } = await params;
// 병렬로 데이터 페칭
const [contentDetail, contentLocations] = await Promise.all([
getContentDetail(id),
getContentLocations(id),
]);
return (
<div>
<ContentOverviewHero
contentDetail={contentDetail}
contentLocations={contentLocations}
/>
<LocationImageCarousel contentId={id} />
</div>
);
}- 클라이언트 번들 크기 감소
// 서버 컴포넌트 (번들에 포함되지 않음)
import { heavyLibrary } from 'heavy-library';
export default async function ServerComponent() {
const data = await heavyLibrary.process();
return <ClientComponent data={data} />;
}
// 클라이언트 컴포넌트 (마법의 use client)
'use client';
export function ClientComponent({ data }) {
return <div>{data}</div>;
}
4. 결과 및 남은 과제들
뭔가 우당탕탕 하고 있어서 아직 성능개선도 많이 하지 못했고, 인프라쪽도 심도있게 관리하지 못하고 있는상황이다. 다만 확실히 Next.js의 기능들을 활용하니 FE개발자의 숙명인 lighthouse 챌린지는 원활히 이룰 수 있을것 같다.
lighthouse 지표 개선 확인
- 기존 cloudfront 지표

- Nextjs 지표

- 결과 분석
확실히 모든면에서 개선된 점을 확인할 수 있었다. LCP등의 지표향상으로 Performace점수가 높아졌고, SEO 점수를 100점 만들었다는 점에서 목표는 달성했다고 볼 수 있을것 같다. 다만 아직 vite나 next모두 구현에만 초점을 두고 최적화를 신경못쓰고 있었기 때문에 이를 개선하는게 남은 가장큰 과제이다.
향후 목표
- Nextjs 또한 CI/CD 배포라인을 형성해야한다.
- Docker, K8s를 사용한 컨테이너화 배포, 인프라 관리 과제가 남아있다.
- FE로서 랜더링 최적화, 성능 개선, 캐싱 전략들을 고민해봐야함.
- 프로젝트 진행 중에 글을 100편은 쓴 것 같은데… 내 블로그로 추려서 옮기는 것도 고민…
- 회고를 이렇게 하는게 맞나? 앞으로 3편의 회고를 더 쓸 것 같은데, 아마 컨테이너화 배포 회고, 이번 회고말고도 추가Nextjs회고, git flow 정복기?, 또 큰 그림 그리고 있는 부분이 있는데 확신은 못하지만 가능하다면 최종 회고는 그 부분을 다루지 않을까 싶다.