프레임워크 내부 들여다보기
프레임워크 코드는 마법이 아니에요. 각각은 특정 문제를 해결하기 위한 단순한 코드의 집합이에요.
프레임워크가 ’알아서 해준다’고 하면 뭔가 대단한 마법처럼 느껴져요. 하지만 실제 코드를 보면 생각보다 단순해요. Next.js 내부 코드를 살펴보면서 프레임워크가 실제로 무엇을 하는지 확인해볼게요.
파일 기반 라우팅
pages/ 폴더에 파일을 만들면 자동으로 라우트가 돼요. 어떻게 가능할까요?
// 폴더 구조로 라우트 구성하기 (간략화된 버전)
const pages = import.meta.glob("./pages/*.tsx", { eager: true });
const routes = Object.keys(pages).map((path) => {
const name = path.match(/\.\/pages\/(.*)\.tsx/)?.[1] ?? "";
return {
name,
path: `/${name === "index" ? "" : name}`,
component: pages[path].default,
};
});
TypeScript
복사
import.meta.glob은 빌드 도구(Vite 등)가 제공하는 기능으로, 패턴에 맞는 파일들을 한 번에 가져와요. 파일 경로에서 라우트 이름을 추출하고, 해당 컴포넌트와 연결하면 끝이에요.
핵심: “파일 시스템을 라우트 설정으로 사용한다”는 발상의 전환이에요. 별도의 라우트 설정 파일 대신, 폴더 구조 자체가 URL 구조가 돼요.
데이터 페칭 (getServerSideProps)
getServerSideProps를 export하면 서버에서 실행돼요. 어떻게?
// Next.js 내부: 페이지 컴포넌트 모듈 로드
const ComponentMod = await requirePage(pathname, distDir, serverless, isAppPath)
// export된 함수들 추출
const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod
TypeScript
복사
Next.js는 페이지 컴포넌트 파일을 로드한 뒤, export된 함수 중 특정 이름을 가진 것들을 뽑아내요. 그리고 요청이 들어오면 해당 함수를 실행해요:
// 추출한 함수 실행
data = await getServerSideProps({
req: req,
res: res,
query,
resolvedUrl: renderOpts.resolvedUrl,
...(pageIsDynamic ? { params } : undefined),
})
TypeScript
복사
핵심: getServerSideProps가 마법처럼 동작하는 게 아니에요. Next.js가 우리 코드를 가져가서 적절한 시점에 실행해주는 것뿐이에요. 이게 “프레임워크가 내 코드를 호출한다”의 의미예요.
이미지 최적화
next/image는 이미지 src를 어떻게 처리할까요?
// Image 컴포넌트 내부의 defaultLoader
function defaultLoader({ config, src, width, quality }) {
return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}`
}
TypeScript
복사
원본 이미지 URL을 /_next/image?url=...&w=800&q=75 형태로 변환해요. 브라우저는 이 URL로 요청하고, Next.js 서버가 이를 받아서 이미지를 최적화해요.
최적화된 이미지는 캐시에 저장돼요:
// 이미지 캐시 로직
const cacheEntry = await this.imageResponseCache.get(
cacheKey,
async () => {
const { buffer, contentType, maxAge } =
await this.imageOptimizer(req, res, paramsResult)
const etag = getHash([buffer])
return {
value: { kind: 'IMAGE', buffer, etag, extension },
revalidate: maxAge
}
}
)
TypeScript
복사
같은 이미지에 대한 요청이 다시 오면 캐시된 결과를 반환해요. 이미지 처리는 CPU를 많이 쓰는 작업이라 캐싱이 중요해요.
프레임워크 코드는 특별하지 않다
프레임워크 내부를 보면 “별거 아니네?”라는 생각이 들 수 있어요. 맞아요. 각각의 코드는 특별하지 않아요.
하지만 이런 것들을 직접 구현하려면:
- 파일 시스템 감시와 라우트 자동 생성
- 빌드 타임과 런타임 분리
- 코드 스플리팅과 번들 최적화
- 개발 서버와 HMR
- 에러 핸들링과 폴백
이 모든 걸 조합하고, 엣지 케이스를 처리하고, 성능을 최적화하는 게 진짜 어려운 일이에요. 프레임워크는 이 조합의 산물이에요.
핵심: 프레임워크의 가치는 개별 기능이 아니라 “조합과 검증”에 있어요.
프레임워크마다 다른 건 뭔가
Next.js, Remix, Nuxt 등 여러 프레임워크가 있어요. 이들이 해결하는 문제는 같지만, 접근 방식이 달라요.
데이터 페칭
// Next.js Pages Router
export async function getServerSideProps({ params }) {
const data = await fetchData(params.id);
return { props: { data } };
}
// Next.js App Router
async function Page({ params }) {
const data = await fetchData(params.id);
return <div>{data}</div>;
}
// Remix
export async function loader({ params }) {
return json(await fetchData(params.id));
}
TypeScript
복사
방식은 달라도 본질은 같아요. “렌더링 전에 데이터를 가져와서 컴포넌트에 전달한다.”
라우팅
Next.js Pages Router: pages/posts/[id].tsx
Next.js App Router: app/posts/[id]/page.tsx
Remix: app/routes/posts.$id.tsx
Nuxt: pages/posts/[id].vue
Plain Text
복사
파일 구조와 네이밍 규칙이 다르지만, 모두 “파일 시스템 = URL 구조”라는 아이디어예요.
프레임워크를 이해하는 방법
프레임워크 문서를 읽을 때 이렇게 질문해보세요.
1.
이 기능은 어떤 문제를 해결하는가?
•
getServerSideProps → 렌더링 전 데이터 페칭
•
'use client' → 클라이언트 전용 컴포넌트 선언
2.
이 기능은 언제 실행되는가?
•
빌드 타임? 요청 시? 클라이언트에서?
•
getStaticProps는 빌드 타임, getServerSideProps는 요청 시
3.
이 기능의 제약 조건은 무엇인가?
•
대부분 직렬화 관련 → “반환값이 직렬화 가능해야 한다”
4.
이 기능 없이 직접 구현하면 어떻게 되는가?
•
이 문서에서 한 것처럼 직접 구현해보면 프레임워크가 뭘 해주는지 명확해져요
핵심: 프레임워크 기능을 이해하려면 “무엇을, 언제, 어떤 제약 조건으로” 해결하는지 파악하세요.
