아티클
순수 프런트 엔드를 사용하여 "동적 + 기사" 다국어 블로그 만들기
내 웹 사이트를 "동적 + 기사" 다국어 블로그로 만들었습니다. 콘텐츠는 Markdown으로 관리되고, 게시 시 번역이 완료되고, 구축 중에 정적 페이지가 생성되고, 브라우저가 언어 읽기 및 전환을 담당합니다. 이 기사에서는 콘텐츠 모델, 공유 파이프라인, 주문형 로딩과 SEO 간의 균형, 그리고 순수 프런트엔드가 정확히 무엇을 절약하는지 분석합니다.
· 9분 분량
내 웹사이트가 생각의 한 문단을 적거나 완전한 기술적 문제를 명확하게 적는 데 사용될 수 있기를 바랍니다. 짧은 것은 '다이내믹스', 긴 것은 '기사'를 입력하세요. 둘은 서로 연결될 수 있으며 방문자가 언어를 전환하면 콘텐츠도 함께 전환되어야 합니다.
이 요구 사항은 매우 평범해 보이지만 처음부터 데이터베이스, 백엔드, 콘텐츠 API를 추가하고 각 언어의 버전을 처리하면 곧 장기적인 유지 관리가 필요한 또 다른 제품이 될 것입니다.
먼저 실제로 사용할 수 있는 요구 사항으로 범위를 좁히기로 결정했습니다. **한 명의 작성자, Git에 콘텐츠를 입력하고 게시 후 웹 사이트를 다시 구축합니다. 리더에는 정적 파일과 브라우저만 필요합니다. ** 현재 stephen.taipei의 "동적 + 기사" 구조입니다.
순수 프론트엔드는 읽기 엔드를 의미합니다.#
먼저 경계를 명확하게 합시다. 이 프로젝트에서는 React, Vite 및 Tailwind를 사용합니다. 콘텐츠 처리, 이미지 변환 및 스크립트 생성에는 여전히 Node.js가 필요합니다. 번역은 출판 단계에서 외부 서비스도 호출합니다. 배포 후 Nginx에서 정적 파일을 제공합니다.
따라서 순수 프런트엔드란 독자가 웹사이트를 열 때 정보를 검색하거나 페이지를 그룹화하거나 실시간으로 번역하기 위해 특별한 블로그 백엔드가 필요하지 않다는 의미입니다. 빌드 프로세스와 호스트는 있지만 기사를 읽기 위한 영구 애플리케이션 API나 데이터베이스는 없습니다.
작업이 미리 완료되면 읽기 끝이 훨씬 쉬워집니다.
뉴스와 기사는 동일한 소스를 공유하며 각각 고유한 모델이 있습니다.#
내용은 content/에 집중되어 있으며, 원문은 모두 Markdown입니다. 두 콘텐츠는 동일한 형식으로 정리되어 있지 않습니다.
content/
tags.yaml
tags.locales.yaml
notes/2026/20261004-0944.md
notes/2026/20261004-0944.locales.yaml
articles/frontend-only-multilingual-blog/
index.md
locales.yaml
cover.webp
다이나믹스는 짧은 카드입니다. 파일 이름은 타이페이 시간의 분을 ID로 사용합니다. 아이디어, 학습, 온라인, 부정행위 또는 독서로 표시될 수 있습니다. 매번 제목을 생각할 필요 없이 한 문단만 작성하면 구축이 가능합니다. 500단어를 초과하면 알림이 뜨고, 1,000단어를 초과하면 기사로 전환되어야 합니다.
기사에는 고정된 슬러그, 제목, 초록, 날짜가 있으며 표지와 업데이트 시간도 있을 수 있습니다. 원래 공유 링크가 무효화되는 것을 방지하기 위해 URL은 공개된 후에는 변경되지 않습니다.
둘은 article:를 사용하여 연관됩니다. 업데이트가 필요한 경우 먼저 질문을 기록한 다음 전체 기사에 연결할 수 있습니다. 기사 페이지에는 이를 역으로 참조하는 업데이트가 나열됩니다. 짧은 콘텐츠는 현재 시작점을 유지하고, 긴 콘텐츠는 파생을 마무리하는 역할을 담당합니다. 양쪽에 동일한 전문을 게재할 필요는 없습니다.
콘텐츠 규칙이 빌드 전에 오류를 차단하도록 허용#
Markdown을 그냥 그림 속에 집어넣었다면, 잘못된 날짜, 연결되지 않은 기사, 대체가 없는 이미지 또는 세 개의 유사한 태그로 작성된 동일한 주제 등 많은 문제가 라이브로 전환될 때까지 볼 수 없었을 것입니다.
저는 이러한 규칙을 공유 콘텐츠 파이프라인에 넣었습니다. loadContent 파일을 읽고 Zod로 필드를 확인한 다음 마크다운, 이미지, 언어 및 콘텐츠 관계를 처리합니다. CLI 검사와 Vite 구성 모두 이 파이프라인을 사용하여 "로컬 시스템에서는 작동한다고 말하지만 공식 환경에서는 작동하지 않습니다" 사이의 격차를 줄입니다.
태그는 tags.yaml 등록된 키만 허용합니다. 동적으로 참조되는 기사 및 기타 동역학이 존재해야 합니다. 로컬 미리보기에서는 초안을 읽을 수 있으며 공식 빌드에서는 초안을 제외합니다. 이러한 규칙은 게시할 때 기억하는 데 기반을 두지 않지만 실제로 검사를 차단합니다.
Markdown에도 명확한 출력 경계가 있습니다. raw HTML은 그대로 출력하지 않고, 생성된 HTML은 정화하며, 코드 블록의 구문 강조는 빌드 시 처리합니다. 로컬 상대 경로로 참조한 콘텐츠 이미지는 최대 너비 1600px의 WebP로 변환하고 GPS 정보를 포함한 EXIF 메타데이터를 제거합니다. 글, 카드, 공유 미리보기는 변환된 이미지를 함께 사용합니다. 원격 이미지와 루트 절대 경로로 참조한 이미지는 이 로컬 변환 과정을 거치지 않습니다.
다국어 시스템 비용은 기사 게재 시 지불됩니다.#
이 웹사이트는 현재 22개 언어를 지원합니다. 중국어 번체 원문은 한 번만 작성하고, 그 옆의 locales.yaml에는 기타 21개 언어의 제목, 초록, 본문, 그림 설명이 저장되어 있습니다.
제가 중요하게 생각하는 결정은 다음과 같습니다. **독자는 번역을 기다릴 필요가 없습니다. ** content:localize 게시할 때 번역을 생성합니다. 공식 빌드는 기존 파일을 읽기만 하며 배포될 때마다 번역 서비스를 다시 호출하지 않습니다.
원본 텍스트와 번역된 텍스트를 비교하려면 소스 다이제스트를 사용하세요. 원문이 변경되면 이전 번역은 더 이상 최신 버전으로 간주될 수 없습니다. 언어군 중 하나가 누락되었거나 번역이 오래된 경우 content:check는 실패합니다. 번역 프로세스에서는 코드, 인라인 코드 및 링크 대상이 기술 식별자 또는 URL을 번역하지 못하도록 보호합니다.
이는 자동 번역이 사람이 편집하는 수준의 품질을 갖는다는 의미는 아닙니다. 완전성과 업데이트된 일관성을 다루며, 전문적인 언어와 어조에는 여전히 교정이 필요합니다. 변경되지 않은 원본 텍스트는 기존 번역을 유지하며, 다시 실행해도 수동으로 수정된 텍스트는 덮어쓰지 않습니다.
목록에서 초록을 읽고, 클릭하여 기사를 로드하세요.#
콘텐츠가 증가한 후 홈페이지가 각 기사 및 각 언어군의 전체 텍스트를 로드하면 순수 프런트엔드가 매우 느려집니다.
Vite plugin은 콘텐츠를 세 가지 진입점으로 나눕니다. 홈페이지용 virtual:content/latest, 콘텐츠 페이지용 virtual:content/index, 그리고 글마다 하나씩 있는 virtual:content/article/<slug>입니다. 홈페이지에는 최신 게시물 세 개와 글 카드 세 개가 표시됩니다. 게시물 데이터에는 전문과 번역문도 들어 있지만 카드에서는 일부만 잘라 보여 줍니다. 장문 글 카드는 요약 등의 데이터만 담고, 전체 본문은 dynamic import로 글마다 따로 불러옵니다.
React의 Suspense는 텍스트 대기를 담당하고, 외부 오류 경계는 로딩 실패 메시지 표시를 담당합니다. 텍스트 요청이 실패하면 제목, 초록, 페이지 탐색이 그대로 유지됩니다.
여기에도 절충점이 있습니다. 현재 기사당 하나의 텍스트 덩어리가 있으며 여기에는 각 언어군의 번역이 포함되어 있으며 아직 언어군당 하나의 덩어리로 분할되지 않았습니다. 동적 전체 텍스트 및 번역이 콘텐츠 색인에 입력됩니다. 콘텐츠 양이 지속적으로 증가함에 따라 인덱스 크기와 다국어 텍스트 다운로드 횟수는 다음 측정 단계입니다.
프론트엔드 스위칭과 정적 읽기를 위한 입구가 있어야 합니다.#
저희는 SPA만 하고, 공유하면 홈페이지에 제목과 사진만 보이는 경우가 많습니다. 이 기사로는 충분하지 않습니다.
따라서 설정 시 각 동적, 기사 및 태그 URL에는 제목, 설명, 표준, 오픈 그래프 및 JSON-LD가 포함된 자체 HTML 셸이 있으며 읽을 수 있는 원본 텍스트는 noscript에 배치됩니다. JavaScript를 실행하지 않고도 이 기사의 내용과 독점 표지를 얻을 수 있습니다.
동일한 콘텐츠는 feed.xml, sitemap-content.xml, 기사의 마크다운 버전 및 llms.txt에 대한 기사 색인도 생성합니다. 이는 수동 동기화가 필요한 또 다른 콘텐츠 세트가 아니라 구축된 제품입니다.
그러나 다국어 SEO의 경계도 명확해야 합니다. **현재 업데이트와 기사는 각 언어에서 동일한 URL을 공유하며 언어는 프런트 엔드에 의해 전환됩니다. 정적 콘텐츠 페이지, 피드 및 Markdown 버전은 원본 텍스트를 유지합니다. ** 홈페이지에는 언어와 밝고 어두운 모드에 대한 사전 렌더링도 있습니다. 이는 각 기사에 독립적으로 색인을 생성할 수 있는 언어 페이지가 22개 있다는 의미는 아닙니다.
향후 검색 엔진에 각 언어군이 포함되도록 허용해야 하는 경우 언어군 라우팅, 해당 정적 페이지 및 hreflang을 추가해야 합니다. 이는 이 세트가 아직 충족하지 못한 또 다른 요구 사항입니다.
포스팅도 전달의 과정이다#
내 출판 과정은 원본 텍스트 작성, 번역 생성, 내용 확인, Git에 제출, 그리고 main에 병합하는 것입니다. 기존 GitHub Actions는 웹사이트를 재구축하고 배포합니다.
npm run content:localize
npm run content:check
npm run test:content
npm run build
다국어 번역이 아직 다 갖춰지지 않았다면 먼저 채웁니다. 글이 초안이면 로컬 미리보기에서만 확인합니다. 정식 출력에서는 초안 페이지가 없는지 확인하고, feed, sitemap, 홈페이지, JavaScript bundle 등 주요 공개 산출물도 검색해 초안 표시가 섞이지 않았는지 검사합니다. 콘텐츠도 코드처럼 diff와 버전 기록이 있어 함께 롤백할 수 있습니다.
이 방법은 실시간 업데이트를 희생합니다. 텍스트 일부를 변경하면 배포를 기다려야 합니다. 그러나 내 기술 웹 사이트의 경우 백엔드가 하나 줄어들고, 실행 기간 종속성이 하나 줄어들고, 변경 기록을 더 쉽게 추적할 수 있는 대가로 이 대기 시간을 교환할 의향이 있습니다.
이 작업을 수행하는 것이 언제 적절한가요?#
작성자라면 게시 빈도를 재구성할 수 있고 콘텐츠가 주로 텍스트이며 Git에 익숙하다면 이 아키텍처가 매우 적합합니다. 짧은 업데이트로 인해 쓰기 임계값이 낮아지고, 기사가 완전한 파생 상태로 유지되며, 다중 언어를 통해 동일한 콘텐츠를 더 많은 사람들이 읽을 수 있습니다.
요구 사항이 여러 사람 간의 실시간 협업, 권한 관리, 독자 댓글, 로그인 또는 구독이 되는 경우 CMS와 백엔드를 재평가해야 합니다. 순수한 프런트엔드는 이러한 요구 사항을 자동으로 사라지게 만들지는 않지만 현재 이 블로그의 범위에 포함하지는 않습니다.
이번 디자인 원칙은 제가 다중 레이아웃 및 다중 테마 아키텍처 설계 패턴에서 쓴 것과 매우 유사합니다. 먼저 책임을 명확하게 분리하고 공유 규칙이 일관성을 유지하도록 하세요.
이 웹사이트의 핵심은 계속해서 글을 쓸 수 있게 하고 게시할 때마다 실제로 어떤 콘텐츠가 전달되고 있는지 알 수 있도록 하는 것입니다.