앱 개요
네트워크 정보(글로벌 IP 주소 등)와 브라우저/디바이스 정보를 빠르게 확인하고 바로 복사/공유할 수 있는 소형 웹 애플리케이션 "Own Info App"을 출시했습니다.
https://x.com/Sloth255000/status/1986792593569763395- 소스(일부 구성): Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
- 배포: S3 + CloudFront (Static Export)
- 다국어: 영어/일본어/중국어/한국어/독일어/프랑스어/스페인어/포르투갈어
- 분석: Firebase Analytics
페이지 전환과 UI는 최대한 심플하게 유지했습니다. 열고, 필요한 정보를 "복사"하거나 "공유"하고, 닫으면 끝입니다.
기술 스택과 설계 포인트
1) Next.js 15 App Router × Static Export
output: 'export'로 완전 정적 출력(SSG)- 출력 파일은
index.html,en.html,ja.html등이 out 디렉터리 바로 아래에 있는 플랫 구조
이 구성으로 CDN 배포와 캐싱이 쉬워지고, 동적 서버나 SSR 없이도 빠른 앱을 구현할 수 있었습니다.
2) CloudFront Functions를 이용한 URL 리라이팅
CloudFront Functions (cloudfront-js-2.0)를 사용해 경량 URL → 실제 파일 매핑을 구현했습니다.
/→/index.html/ja→/ja.html(후행 슬래시도 마찬가지로 처리)/contact,/privacy,/licenses→ 각각의*.html파일- 확장자가 있는 URI(.svg, .js, .css 등)는 리라이팅 없이 통과
- 지원하지 않는 로케일은
/en/_not-found로 리다이렉트
3) i18n 및 라우팅
- 지원 언어:
en, ja, zh, ko, de, fr, es, pt - 번역은
src/locales/*.json으로 분리, App Router에서 각 로케일별 SSG - 루트
/는 SEO 및 봇을 위해 기본 언어(en) 내용을 SSR과 동일하게 렌더링하고, 브라우저 접근 시에만navigator.languages를 기반으로 로케일 페이지로 일회성 클라이언트 리다이렉트 수행
4) SEO 및 OGP/Twitter Cards (로케일별)
- 각 페이지에
generateMetadata()를 구현해 언어별 "canonical / og:url / og:image / og:locale"을 정확하게 출력 - 각 언어별 OGP 이미지(
public/og-*.png)를 준비하고, 이미지 내 설명 텍스트는 번역 텍스트로 제작 - 서버 컴포넌트에서 로케일별 구조화 데이터(JSON-LD) 출력(
application/ld+json). 8개 언어 모두inLanguage에 명시
// 예시: 구조화 데이터 출력 (발췌)
export default function StructuredData({ title, description, siteUrl }: Props) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: title,
description,
url: siteUrl || '/',
applicationCategory: 'UtilityApplication',
operatingSystem: 'Web Browser',
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
inLanguage: [
{ '@type': 'Language', name: 'English', alternateName: 'en' },
{ '@type': 'Language', name: 'Japanese', alternateName: 'ja' },
{ '@type': 'Language', name: 'Chinese', alternateName: 'zh' },
{ '@type': 'Language', name: 'Korean', alternateName: 'ko' },
{ '@type': 'Language', name: 'German', alternateName: 'de' },
{ '@type': 'Language', name: 'French', alternateName: 'fr' },
{ '@type': 'Language', name: 'Spanish', alternateName: 'es' },
{ '@type': 'Language', name: 'Portuguese', alternateName: 'pt' },
],
};
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />;
}
5) 로케일별 올바른 <html lang>
App Router에서는 "루트 레이아웃의 <html>이 모든 페이지에 공통"이라는 제약이 있어, 빌드 시점에 <html lang="en">이 남게 됩니다.
이번에는 로케일 레이아웃과 루트 페이지 양쪽에 "클라이언트 측에서 <html lang>을 덮어쓰는" 얇은 컴포넌트를 추가해, 실제 사용 시 올바른 언어 태그로 업데이트하도록 구현했습니다.
'use client'
import { useEffect } from 'react'
export default function HtmlLangSetterClient({ lang }: { lang: string }) {
useEffect(() => {
document.documentElement.setAttribute('lang', lang)
}, [lang])
return null
}
빌드 시 정적 HTML과 실제 사용자가 보는 DOM의 차이를 최소화하면서 Static Export 제약 하에 실용적인 구현을 달성했습니다.
배포/운영
인프라
- S3 (정적 호스팅) + CloudFront (오리진은 S3의 Website 엔드포인트)
- CloudFront Functions는 별도 배포(리소스 생성용 SAM 템플릿, 코드 업데이트는 CI 측에서)
CI/CD (GitHub Actions)
main브랜치 push 시 자동 실행:- Next.js 빌드(Static Export)
- S3 동기화
/_next/static은max-age=31536000, immutable- HTML은
max-age=300, must-revalidate
- CloudFront Function 코드 업데이트(
update-function→publish-function) - 필요 시 CloudFront Invalidation(
/*)
AWS 자격 증명, S3 버킷, CloudFront Distribution ID, NEXT_PUBLIC_*는 Secrets/Vars로 설정되어 있습니다.
CLI를 이용한 수동 업데이트도 가능하지만 기본적으로 CI에 맡기고 있습니다.
정리
- 현지화
- 최소한의 SEO
- 최소한의 인프라 구성
- CI/CD & IaC
개인 개발을 시작하면서 무엇이든 앱을 하나 출시하는 전체 과정을 경험해보고 싶었기 때문에 이런 심플하고 저비용의 앱이 되었습니다.
처음으로 Next.js를 사용해볼 수 있어서 좋았습니다. 다음에는 SSG 대신 SSR로 Vercel 배포도 시도해보겠습니다.
