홈페이지를 처음 만들 때 글이 몇 편이면 제목과 본문을 한 파일에 넣어도 관리하기 쉽다. 그런데 글을 계속 발행하면 첫 화면을 열기 위해 읽지도 않을 본문까지 준비하는 구조가 될 수 있다. 고객이 원하는 것은 글을 많이 올릴 수 있는 사이트이지, 글이 늘 때마다 첫 방문의 부담도 같이 커지는 사이트는 아니다.
내가 만든 지속 발행 사이트에서는 검색과 분류에 필요한 목록 정보와 상세 본문을 나눴다. 이번에 현재 원본을 다시 읽고 별도 폴더로 빌드해 보니, 발행 목록 83개가 각각의 본문과 연결되고 본문 83개가 독립 청크로 생성됐다. 첫 진입 파일의 즉시 의존성에는 그 본문 청크가 포함되지 않았다. 여기서 확인한 것은 파일 분리 구조이며, 방문자가 몇 초 빨라졌다는 결과는 측정하지 않았다.
나는 목록에서 필요한 정보부터 좁혔다
글을 고를 때는 제목과 짧은 설명, 분류, 발행 상태, 수정일 같은 정보가 필요하다. 긴 단계별 설명과 비교표, 자주 묻는 질문은 그 글을 열었을 때 읽는 정보다. 이 둘을 나누면 첫 화면과 검색 목록이 전체 본문을 알 필요가 줄어든다.
분리하기 전에 검색이 어디까지 찾아야 하는지 정해야 한다. 제목과 요약을 찾는 검색은 가벼운 목록으로 구현할 수 있다. 본문 구석의 모든 문장까지 찾아야 한다면 별도 검색 색인이나 서버 검색이 필요할 수 있다. 나는 목록 검색을 만들어 놓고 본문 전체 검색이 된다고 설명하지 않는다. 검색 범위를 방문자와 운영자가 이해할 수 있게 정한다.
본문은 글을 열 때 불러오는 연결로 바꿨다
현재 소스에는 글 주소에 대응하는 명시적인 로더 목록이 있다. 각 로더는 해당 본문 파일을 동적으로 가져온다. 목록은 작은 메타데이터를 사용하고, 상세 경로에서는 주소에 맞는 본문을 요청하는 구조다. 모든 본문을 첫 진입 파일에 직접 import하는 방식과 구별되는 지점이다.
하지만 파일을 나누었다고 첫 화면 전체가 가벼워지는 것은 아니다. 공통 UI, 큰 이미지, 첫 화면의 도구와 라이브러리는 별도 부담을 만들 수 있다. 글 상세에서도 홈 기능이 함께 들어오는 구조라면 그것도 따로 살펴야 한다. 나는 본문 분리의 결과와 홈페이지 전체 성능을 같은 결론으로 합치지 않는다.
나는 제목은 있는데 본문은 없는 상태도 검사했다
분리하면 새로 생기는 실수도 있다. 목록에는 발행됐는데 로더를 추가하지 않았거나, 본문 파일의 주소 이름이 목록과 다르거나, 내린 글이 관련 글에 남을 수 있다. 화면은 깔끔해도 클릭한 뒤 읽을 내용이 없으면 콘텐츠 관리가 끊긴 것이다.
이번 검사는 발행 목록의 모든 항목에 로더가 있는지, 실제로 읽은 본문의 slug가 같은지, 본문 섹션이 있는지를 확인했다. 빌드 결과의 독립 본문 수와 목록 수도 대조했다. 모르는 주소는 null을 돌려주는 동작을 확인했다. 실제 브라우저의 네트워크 요청과 검색 노출은 이 검사에 포함하지 않았다.
- 발행 목록에 있는 주소마다 본문을 여는 연결이 있는가.
- 불러온 본문의 주소 이름이 목록과 같은가.
- 본문이 비어 있지 않고 필요한 구조를 갖추었는가.
- 공개하지 않은 글은 목록과 관련 글에서 제외되는가.
- 빌드 결과에서도 본문이 첫 진입의 즉시 의존성에 합쳐지지 않았는가.
분리한 본문에도 발행 경계를 둔다
다음은 확인한 구조를 작게 풀어 쓴 합성 예시다. 공개 목록에 있는 글만 로더를 선택하고, 목록과 본문이 맞는지 다시 확인한다. 없는 주소와 불러오기 실패를 같은 성공 상태로 만들지 않는다. 독자가 여러 홈페이지에 적용할 때 핵심 조건을 읽을 수 있도록 주변 UI는 제외했다.
이 코드의 공개 목록 검사는 비공개 자료의 보안 장치가 아니다. 초안 파일이 정적 빌드에 포함되면 주소를 아는 사람이 직접 요청할 수 있다. 비공개 원고와 내부 근거는 처음부터 공개 산출물에서 제외하거나 인증된 저장소에 두어야 한다. 메뉴에서 숨기는 것과 파일 접근을 막는 것은 따로 확인한다.
코드와 구현 살펴보기
JavaScript · 공개 메타데이터와 명시적인 본문 로더
// Synthetic example: metadata is small; bodies have explicit loaders.
const published = new Map([
['first-guide', { title: 'First guide', status: 'published' }],
['second-guide', { title: 'Second guide', status: 'published' }]
]);
const loaders = {
'first-guide': () => import('./bodies/first-guide.js'),
'second-guide': () => import('./bodies/second-guide.js')
};
export async function loadPublishedArticle(slug) {
const metadata = published.get(slug);
if (!metadata || metadata.status !== 'published') return null;
const load = Object.hasOwn(loaders, slug) ? loaders[slug] : null;
if (!load) throw new Error('Published article has no body loader');
const module = await load();
const body = module.default;
if (!body || body.slug !== slug || !Array.isArray(body.sections)) {
throw new Error('Article body does not match its metadata');
}
return { ...metadata, ...body };
}
// The route distinguishes null (not published) from a loading failure.
// The explicit map is not an authorization boundary for private files.로딩 중과 실패 뒤의 읽기 흐름도 필요하다
본문을 나중에 가져오면 로딩 중 상태가 생긴다. 나는 제목과 안내를 유지하고, 본문을 준비 중이라는 상태를 알리며, 실패하면 재시도와 목록으로 돌아가는 길을 두는 쪽으로 설계한다. 로딩 실패를 빈 글처럼 보이게 하면 방문자가 내용이 없는 것인지 연결이 끊긴 것인지 알기 어렵다.
또 글을 빠르게 바꾸어 열었을 때 이전 요청이 늦게 끝나 현재 제목 아래에 다른 본문을 넣지 않게 해야 한다. UI에서는 현재 주소와 요청의 순서를 확인하거나 사용 중인 프레임워크의 취소·정리 기능으로 오래된 결과를 무시한다. 이 글의 작은 로더는 데이터 선택만 담당하며, 이런 화면 상태 관리까지 완료한 공통 UI라고 제시하지 않는다.
검색 가능한 문서와 가벼운 진입은 함께 확인한다
클릭한 뒤 자바스크립트로 본문이 나타난다고 검색에 필요한 문서가 자동으로 완성되는 것은 아니다. 내 작업에는 별도 정적 문서 생성 구조도 있다. 글별 제목과 본문, 대표 주소, 사이트맵을 직접 열어 확인하는 절차가 본문 파일 분리와 함께 필요하다.
이번에 다시 실행한 것은 클라이언트 빌드와 본문 연결 검사다. 정적 문서 생성 전체와 실제 검색 등록을 재검증한 것은 아니다. 새 프로젝트에서는 상세 주소로 바로 들어갔을 때 본문이 존재하는지, 새로고침해도 같은 글인지, 내린 글이 공개 목록과 검색용 산출물에서 빠지는지를 별도로 확인한다. 검색 결과나 순위가 생겼다는 주장은 하지 않는다.
나는 분리의 성과를 측정 가능한 말로 남긴다
내가 이번에 확인한 결과는 발행 목록과 본문의 연결, 독립 빌드 파일, 첫 진입의 즉시 의존성이다. 이는 글이 늘어날 때 어떤 정보를 처음부터 준비할지 선택한 구조의 근거다. 통신 속도, 기기 성능, 캐시, 이미지 크기까지 포함한 실제 체감 속도는 다른 측정이다.
그래서 나는 몇 퍼센트 빨라졌다는 숫자보다 먼저 운영자가 지켜야 할 조건을 남긴다. 글을 추가할 때 목록과 본문을 같이 등록하고, 발행 상태를 같이 바꾸고, 연결 검사를 다시 한다. 필요하면 이후 같은 조건의 기기와 네트워크에서 처음 방문과 재방문을 나누어 측정한다. 글이 늘어나는 사이트는 파일을 나누는 것뿐 아니라 그 연결을 계속 유지할 수 있어야 한다.