Blog
HomePostsTagsCategories
© 2026 Hyeonseok An. All rights reserved.
GitHubEmailInstagram
Home
Develop
[TIL] Tag/Category URL Slug 문제 해결 학습 기록

[TIL] Tag/Category URL Slug 문제 해결 학습 기록

August 20, 2025
Loading...
Next.jsError HandlingSEOTILFront-endURL encodingBlogTrouble shooting
Cover image for [TIL] Tag/Category URL Slug 문제 해결 학습 기록

1. 문제 정의 및 분석

1.1 발견된 문제들

이번 스프린트에서 블로그의 태그와 카테고리 URL 시스템에서 두 가지 주요 문제가 발견되었습니다.

문제 1: 대/소문자 구분 이슈

  • 현상: Tag가 "Proxmox"인 경우, 정적으로 생성된 페이지는 /tag/proxmox로 생성됨
  • 문제점: 포스트 미리보기 카드의 tag badge href가 태그 원본 그대로 /tag/Proxmox로 설정
  • 결과: 사용자가 태그를 클릭하면 404 오류 발생

문제 2: 한글 태그/카테고리 URL 접근 실패

  • 현상: "홈서버", "개발 블로그" 등 한글 태그가 포함된 포스트에서 문제 발생
  • 문제점 1: 포스트 미리보기 카드의 tag badge href가 정상적으로 설정되지 않음
  • 문제점 2: /tag/홈서버와 같이 한글로 직접 URL 접근해도 404 발생
  • 근본 원인: 기존 slugify 함수가 한글 문자를 제거해버림

1.2 문제의 근본 원인 분석

1.2.1 slugify 함수의 한계

// 기존 문제가 있던 코드 export function slugify(text: string): string { return text .toLowerCase() .normalize('NFD') // 한글을 자소 단위로 분해 .replace(/[\\u0300-\\u036f]/g, '') // 분해된 자소 제거 .replace(/[^\\w\\s-]/g, '') // 한글 문자까지 제거 .replace(/[\\s_-]+/g, '-') .replace(/^-+|-+$/g, '') }
  • normalize('NFD')가 한글을 자소 단위로 분해 ("홈" → "ㅎ", "ㅗ", "ㅁ")
  • replace(/[^\\w\\s-]/g, '')가 분해된 한글 자소들을 특수문자로 인식하여 제거
  • 결과적으로 한글 태그는 빈 문자열이 되어 fallback으로 "untitled"이 됨

1.2.2 PostCard 컴포넌트의 href 설정 문제

// 문제가 있던 코드 (4개 variant 모두 동일한 패턴) <Link key={tag} href={`/tag/${tag}`} // 원본 태그명 그대로 사용 className={getTagClasses('compact')} > {tag} </Link>
  • 태그 href 생성 시 원본 태그명을 그대로 사용
  • slugify 적용 없이 대/소문자와 특수문자가 그대로 URL에 포함
  • 정적 생성된 페이지 경로와 불일치 발생

1.2.3 URL 디코딩 처리 부재

// 기존 태그 찾기 로직 const tag = tags.find(t => t.slug === slug)
  • 브라우저에서 한글 URL 입력 시 자동으로 URL 인코딩 (홈서버 → %ED%99%88%EC%84%9C%EB%B2%84)
  • 서버에서 받는 slug 파라미터와 DB의 slug 직접 비교로 인한 매칭 실패
  • URL 디코딩이나 정규화 과정 없이 단순 문자열 비교만 수행

1.2.4 정적 페이지 생성과 동적 라우팅 불일치

  • generateStaticParams()에서는 올바른 slug로 정적 페이지 생성
  • 실제 페이지 접근 시에는 URL 파라미터와 DB 데이터 간 매칭 실패
  • 다양한 URL 접근 패턴에 대한 fallback 로직 부재

2. 기술적 배경 지식

2.1 Next.js Dynamic Routing 이해

2.1.1 [slug] 파라미터 처리 방식

Next.js의 동적 라우팅에서 [slug] 파라미터는 URL의 동적 세그먼트를 캡처합니다.
// /tag/[slug]/page.tsx interface TagPageProps { params: Promise<{ slug: string // URL에서 추출된 slug 파라미터 }> }
중요한 점은 브라우저가 URL을 어떻게 처리하느냐입니다:
  • 영문 대소문자: /tag/Proxmox → slug는 "Proxmox" 그대로
  • 한글: /tag/홈서버 → slug는 "홈서버" 또는 URL 인코딩된 형태

2.1.2 generateStaticParams와 ISR의 관계

export async function generateStaticParams() { const tags = await getAllTags() return tags.map(tag => ({ slug: tag.slug, // slugify된 값들 })) } export const revalidate = 3600 // ISR: 1시간마다 재검증
  • 빌드 타임에 모든 태그에 대해 정적 페이지 생성
  • tag.slug는 이미 slugify된 값 (예: "proxmox", "홈서버")
  • ISR로 새로운 태그 추가 시에도 페이지 생성

2.1.3 notFound() 함수 동작 원리

if (!tag) { notFound() // Next.js built-in 404 처리 }
  • 태그를 찾지 못하면 즉시 404 페이지로 리다이렉트
  • 사용자에게는 "This page could not be found" 메시지 표시
  • 이전 로직에서는 대소문자나 URL 인코딩 불일치로 인해 빈번히 발생

2.2 Unicode와 URL 인코딩

2.2.1 한글 문자의 Unicode 범위

// 한국어 완성형 한글 (가-힣) \\uAC00-\\uD7AF // 44032부터 55215까지
주요 한글 Unicode 블록:
  • \uAC00-\uD7AF: 완성형 한글 (가, 나, 다, ... , 힘, 힙, 힣)
  • \u1100-\u11FF: 한글 자모 (ㄱ, ㄴ, ㄷ, ...)
  • \u3130-\u318F: 호환용 한글 자모 (ㄱ, ㄴ, ㄷ, ...)
우리가 주로 사용하는 것은 완성형 한글이므로 \uAC00-\uD7AF 범위가 가장 중요합니다.

2.2.2 URL 인코딩/디코딩

// 한글 "홈서버"의 URL 인코딩 과정 "홈서버" → UTF-8 바이트: [ED 99 88] [EC 84 9C] [EB B2 84] → URL 인코딩: "%ED%99%88%EC%84%9C%EB%B2%84" // JavaScript에서의 처리 encodeURIComponent("홈서버") // "%ED%99%88%EC%84%9C%EB%B2%84" decodeURIComponent("%ED%99%88%EC%84%9C%EB%B2%84") // "홈서버"
브라우저는 주소창에 한글을 입력하면 자동으로 URL 인코딩을 수행하므로, 서버에서는 이를 디코딩해야 합니다.

2.2.3 정규표현식에서의 Unicode 플래그 사용법

// Unicode 플래그 없이 (문제 발생) /[a-z0-9\\s\\uAC00-\\uD7AF-]/g // SyntaxError 발생 가능 // Unicode 플래그와 함께 (올바른 사용법) /[a-z0-9\\s\\uAC00-\\uD7AF-]/gu // 정상 작동 // 하이픈 위치 주의 (문자 클래스 내에서) /[a-z0-9\\s-\\uAC00-\\uD7AF]/gu // 하이픈을 끝에 위치

2.3 Slug 생성과 검증

2.3.1 SEO 친화적 URL 생성 원칙

좋은 slug의 특징:
  • 가독성: 사용자가 URL만 봐도 내용을 추측할 수 있음
  • 일관성: 동일한 규칙으로 생성된 일관된 패턴
  • 간결성: 불필요한 단어나 문자 제거
  • 검색 엔진 최적화: 키워드 포함과 적절한 길이
// 좋은 slug 예시 "Next.js Development" → "nextjs-development" "Docker & Kubernetes" → "docker-kubernetes" "홈서버 구축하기" → "홈서버-구축하기" // 피해야 할 slug "Next.js Development!!!" → "nextjs-development" // 특수문자 제거 " multiple spaces " → "multiple-spaces" // 공백 정규화

2.3.2 다국어 지원을 위한 slug 설계

한국어 블로그에서 고려해야 할 사항:
  • 한글 문자 보존: SEO와 사용자 경험을 위해 한글 유지
  • 영문과의 혼용: "React 개발기"와 같은 혼합 태그 처리
  • 길이 제한: 한글은 바이트 수가 크므로 적절한 길이 제한 필요
// 다국어 slug 생성 전략 export function slugify(text: string): string { const slug = text .toLowerCase() // 한글, 영문, 숫자, 하이픈만 허용 .replace(/[^a-z0-9\\s\\uAC00-\\uD7AF-]/gu, '') .replace(/[\\s_-]+/g, '-') // 공백을 하이픈으로 .replace(/^-+|-+$/g, '') // 앞뒤 하이픈 제거 return slug || 'untitled' // 빈 문자열 fallback }

2.3.3 URL 안전성과 접근성 고려사항

  • 브라우저 호환성: 모든 주요 브라우저에서 정상 작동
  • 서버 인코딩: 서버가 UTF-8을 올바르게 처리하는지 확인
  • 길이 제한: URL 길이 제한 (일반적으로 2000자 이하 권장)
  • 특수문자 처리: URL에서 의미가 있는 문자들 (?&=# 등) 제거

3. 해결 과정

3.1 1단계: 문제 재현 및 디버깅

3.1.1 개발 환경에서의 문제 재현

문제를 체계적으로 파악하기 위해 다음과 같은 테스트를 수행했습니다:
// 테스트 케이스 작성 const testCases = [ { input: "Proxmox", expected: "proxmox" }, { input: "DOCKER", expected: "docker" }, { input: "홈서버", expected: "홈서버" }, { input: "개발 블로그", expected: "개발-블로그" }, { input: "Next.js Development", expected: "nextjs-development" } ];
각 테스트 케이스에 대해:
  1. 태그 뱃지 클릭 시 이동하는 URL 확인
  1. 해당 URL로 직접 접근 시 응답 확인
  1. 정적 생성된 페이지 경로와 비교

3.1.2 브라우저 네트워크 탭을 통한 요청/응답 분석

Chrome DevTools Network 탭에서 관찰한 내용:
대소문자 문제:
클릭한 링크: /tag/Proxmox 서버 응답: 404 Not Found 실제 존재하는 페이지: /tag/proxmox
한글 문제:
브라우저 주소창: /tag/홈서버 실제 요청 URL: /tag/%ED%99%88%EC%84%9C%EB%B2%84 서버에서 받은 slug: "홈서버" (자동 디코딩됨) DB에서 찾는 slug: tag.slug === "untitled" (slugify 결과)

3.1.3 slug 생성 과정 추적

기존 slug 생성 과정을 단계별로 추적:
// "홈서버" 태그의 slug 생성 과정 추적 console.log('1. 원본:', '홈서버'); console.log('2. toLowerCase:', '홈서버'.toLowerCase()); console.log('3. normalize NFD:', '홈서버'.toLowerCase().normalize('NFD')); console.log('4. 특수문자 제거 후:', ''); // 한글이 모두 제거됨 console.log('5. 최종 결과:', 'untitled');
이를 통해 normalize('NFD')가 한글을 자소 단위로 분해하고, 이후 정규표현식에서 이를 제거하는 것이 문제임을 확인했습니다.

3.2 2단계: PostCard 컴포넌트 수정

3.2.1 태그 href에 slugify() 함수 적용

PostCard 컴포넌트의 모든 태그 링크에 일관된 수정을 적용했습니다:
// 수정 전: 원본 태그명 사용 <Link key={tag} href={`/tag/${tag}`} className={getTagClasses('compact')} > // 수정 후: slugify 적용 import { slugify } from '@/lib/slug-utils' <Link key={tag} href={`/tag/${slugify(tag)}`} className={getTagClasses('compact')} >

3.2.2 모든 카드 variant에 대한 일관된 수정

PostCard 컴포넌트는 4가지 variant를 지원하므로 모든 경우에 동일한 수정을 적용:
  1. minimal variant: 2개 태그 표시
  1. featured variant: 4개 태그 표시 + 컬러풀 스타일
  1. compact variant: 2개 태그 표시 + 컴팩트 레이아웃
  1. default variant: maxTags까지 표시
각각의 태그 렌더링 부분에서 동일한 패턴으로 수정:
{post.tags.slice(0, maxTags).map(tag => ( <Link key={tag} href={`/tag/${slugify(tag)}`} // 모든 variant에 동일 적용 className={getTagClasses('default')} > {tag} </Link> ))}

3.2.3 Breadcrumbs 컴포넌트 카테고리 링크 수정

PostBreadcrumbs 컴포넌트에서도 동일한 문제가 발견되어 수정:
// 수정 전: 수동으로 slug 생성 href: `/category/${category.toLowerCase().replace(/\\s+/g, '-')}` // 수정 후: 표준 slugify 함수 사용 import { slugify } from '@/lib/slug-utils' href: `/category/${slugify(category)}`

3.3 3단계: slugify 함수 개선

3.3.1 한글 문자 보존 로직 추가

기존 slugify 함수의 문제점을 분석하고 개선된 버전을 구현:
// 개선된 slugify 함수 export function slugify(text: string): string { const slug = text .toLowerCase() // normalize 제거: 한글 분해 방지 // 한글(\\uAC00-\\uD7AF), 영문, 숫자, 공백, 하이픈만 허용 .replace(/[^a-z0-9\\s\\uAC00-\\uD7AF-]/gu, '') .replace(/[\\s_-]+/g, '-') // 공백을 하이픈으로 변환 .replace(/^-+|-+$/g, '') // 앞뒤 하이픈 제거 return slug || 'untitled' // 빈 문자열 fallback }
주요 개선사항:
  • normalize('NFD') 제거로 한글 분해 방지
  • Unicode 플래그 u 추가로 한글 문자 올바른 처리
  • 정규표현식에서 한글 범위 \\uAC00-\\uD7AF 명시적 허용

3.3.2 정규표현식 패턴 최적화

정규표현식 작성 시 주의사항들을 고려한 최적화:
// 하이픈 위치 조정 (문자 클래스 마지막에 위치) /[^a-z0-9\\s\\uAC00-\\uD7AF-]/gu // Unicode 플래그로 올바른 문자 범위 처리 // 'g'는 전역 매칭, 'u'는 Unicode 지원

3.3.3 개선 결과 검증

개선된 함수로 테스트 케이스 재검증:
// 테스트 결과 slugify("Proxmox") // "proxmox" ✓ slugify("홈서버") // "홈서버" ✓ slugify("개발 블로그") // "개발-블로그" ✓ slugify("Next.js Development") // "nextjs-development" ✓ slugify("Docker & Kubernetes") // "docker-kubernetes" ✓

3.4 4단계: 동적 라우팅 페이지 강화

3.4.1 findTagBySlug/findCategoryBySlug 헬퍼 함수 구현

URL 디코딩과 다양한 매칭 전략을 포함한 robust한 태그 찾기 로직을 구현:
// 태그 찾기 헬퍼 함수 function findTagBySlug(tags: TagWithCount[], slug: string): TagWithCount | undefined { // 1단계: 직접 매칭 let tag = tags.find(t => t.slug === slug) if (tag) return tag // 2단계: URL 디코딩 후 매칭 try { const decodedSlug = decodeURIComponent(slug) tag = tags.find(t => t.slug === decodedSlug) if (tag) return tag // 3단계: 디코딩된 slug를 정규화 후 매칭 const normalizedSlug = slugify(decodedSlug) tag = tags.find(t => t.slug === normalizedSlug) if (tag) return tag } catch { // URL 디코딩 실패 시 무시하고 계속 } // 4단계: 이름 기반 매칭 (마지막 resort) tag = tags.find(t => slugify(t.name) === slug) if (tag) return tag return undefined }

3.4.2 다중 fallback 매칭 전략 적용

각 매칭 단계별 처리 시나리오:
1단계 - 직접 매칭:
  • 정적 생성된 페이지나 올바른 링크에서 온 경우
  • 가장 빠른 성능
2단계 - URL 디코딩 매칭:
// 브라우저에서 한글 URL 입력 시 // /tag/홈서버 → slug = "홈서버" (자동 디코딩됨) // /tag/%ED%99%88%EC%84%9C%EB%B2%84 → slug = "%ED%99%88%EC%84%9C%EB%B2%84" const decodedSlug = decodeURIComponent(slug) // "홈서버"
3단계 - 정규화 매칭:
// 예외적인 경우나 레거시 URL 처리 const normalizedSlug = slugify(decodedSlug)
4단계 - 이름 기반 매칭:
// 최후의 수단: 태그 이름을 slugify해서 비교 tag = tags.find(t => slugify(t.name) === slug)

3.4.3 URL 디코딩 및 오류 처리

URL 디코딩 시 발생할 수 있는 오류를 안전하게 처리:
try { const decodedSlug = decodeURIComponent(slug) // 디코딩된 slug로 매칭 시도 } catch { // 잘못된 URL 인코딩이나 디코딩 실패 시 // 다음 단계로 넘어가서 계속 시도 }
이렇게 하면 잘못된 URL이 들어와도 애플리케이션이 크래시되지 않고, 가능한 모든 방법을 시도해서 올바른 태그를 찾으려고 합니다.

4. 구현된 솔루션

4.1 Enhanced Slugify Function

4.1.1 한글 문자 지원 로직

최종적으로 구현된 slugify 함수는 다음과 같은 특징을 가집니다:
export function slugify(text: string): string { const slug = text .toLowerCase() // 한글(\\uAC00-\\uD7AF), 영문자(a-z), 숫자(0-9), // 공백(\\s), 하이픈(-) 만 허용 .replace(/[^a-z0-9\\s\\uAC00-\\uD7AF-]/gu, '') .replace(/[\\s_-]+/g, '-') // 연속된 공백/언더스코어/하이픈을 하이픈 하나로 .replace(/^-+|-+$/g, '') // 시작과 끝의 하이픈 제거 // 빈 문자열이 되는 edge case 처리 return slug || 'untitled' }
핵심 개선사항:
  • normalize('NFD') 제거: 한글 문자 분해 방지
  • 한글 Unicode 범위 추가: \\uAC00-\\uD7AF로 완성형 한글 보존
  • Unicode 플래그 사용: /gu 플래그로 올바른 Unicode 처리

4.1.2 Edge case 처리 (빈 문자열 fallback)

특수한 경우들에 대한 안전한 처리:
// Edge cases 테스트 slugify("!@#$%^&*()") // → "untitled" (모든 문자가 제거됨) slugify(" ") // → "untitled" (공백만 있는 경우) slugify("---") // → "untitled" (하이픈만 있는 경우) slugify("") // → "untitled" (빈 문자열)

4.1.3 isValidSlug 함수 업데이트

slug 검증 함수도 한글을 지원하도록 업데이트:
export function isValidSlug(slug: string): boolean { // 한글을 포함한 올바른 slug 패턴 검증 const slugPattern = /^[a-z0-9\\uAC00-\\uD7AF]+(?:-[a-z0-9\\uAC00-\\uD7AF]+)*$/u return slugPattern.test(slug) && slug.length > 0 && slug.length <= 100 } // 검증 예시 isValidSlug("proxmox") // true isValidSlug("홈서버") // true isValidSlug("nextjs-dev") // true isValidSlug("개발-블로그") // true isValidSlug("Proxmox") // false (대문자) isValidSlug("-invalid") // false (하이픈으로 시작)

4.2 Robust Tag/Category Matching

4.2.1 직접 매칭 → URL 디코딩 → 정규화 → 이름 기반 매칭

4단계 fallback 매칭 시스템의 상세한 동작 방식:
function findTagBySlug(tags: TagWithCount[], slug: string): TagWithCount | undefined { console.log(`🔍 태그 찾기 시작: "${slug}"`) // === 1단계: 직접 매칭 (가장 일반적인 케이스) === let tag = tags.find(t => t.slug === slug) if (tag) { console.log(`✅ 1단계 성공: 직접 매칭`) return tag } // === 2단계: URL 디코딩 후 매칭 === try { const decodedSlug = decodeURIComponent(slug) console.log(`🔧 2단계: URL 디코딩 "${slug}" → "${decodedSlug}"`) // 디코딩된 값으로 직접 매칭 tag = tags.find(t => t.slug === decodedSlug) if (tag) { console.log(`✅ 2단계 성공: 디코딩 후 직접 매칭`) return tag } // === 3단계: 디코딩 + 정규화 매칭 === const normalizedSlug = slugify(decodedSlug) console.log(`🔧 3단계: 정규화 "${decodedSlug}" → "${normalizedSlug}"`) tag = tags.find(t => t.slug === normalizedSlug) if (tag) { console.log(`✅ 3단계 성공: 정규화 후 매칭`) return tag } } catch (error) { console.log(`⚠️ URL 디코딩 실패, 다음 단계로 진행`) } // === 4단계: 이름 기반 매칭 (최후의 수단) === console.log(`🔧 4단계: 이름 기반 매칭 시도`) tag = tags.find(t => slugify(t.name) === slug) if (tag) { console.log(`✅ 4단계 성공: 이름 기반 매칭`) return tag } console.log(`❌ 모든 단계 실패: 태그를 찾을 수 없음`) return undefined }

4.2.2 오류 복구 메커니즘

각 단계에서 실패해도 다음 단계로 넘어가는 안전한 처리:
URL 디코딩 실패 시:
try { const decodedSlug = decodeURIComponent(slug) // ... 디코딩 관련 로직 } catch { // 디코딩 실패해도 애플리케이션은 계속 동작 // 다음 fallback 단계로 자연스럽게 이동 }
예외 상황 처리 예시:
  • 잘못된 URL 인코딩: %XX 형태가 올바르지 않은 경우
  • 부분적 인코딩: 홈%EC%84%9C%EB%B2%84 같은 혼합 형태
  • 레거시 URL: 이전 버전에서 다른 방식으로 생성된 URL

4.2.3 성능 최적화 고려사항

효율적인 매칭을 위한 최적화 전략:
조기 반환 (Early Return):
// 각 단계에서 찾으면 즉시 반환 let tag = tags.find(t => t.slug === slug) if (tag) return tag // 더 이상 처리하지 않음
비용이 큰 연산 최소화:
// URL 디코딩은 try-catch 안에서만 수행 // slugify 연산도 필요한 경우에만 수행
메모리 효율성:
// 임시 변수 재사용으로 메모리 할당 최소화 let tag = tags.find(...) // 변수 재사용

4.3 Component Architecture Improvements

4.3.1 중복 코드 제거와 재사용성 향상

기존에는 각 컴포넌트마다 다른 방식으로 slug를 생성했지만, 이제는 통일된 방식을 사용:
Before (중복 코드):
// PostCard.tsx에서 href={`/tag/${tag}`} // Breadcrumbs.tsx에서 href={`/category/${category.toLowerCase().replace(/\\s+/g, '-')}`} // 다른 컴포넌트에서 href={`/tag/${tag.replace(/\\s/g, '-').toLowerCase()}`}
After (통일된 방식):
// 모든 컴포넌트에서 동일하게 import { slugify } from '@/lib/slug-utils' href={`/tag/${slugify(tag)}`} href={`/category/${slugify(category)}`}

4.3.2 TypeScript 타입 안전성 강화

헬퍼 함수에 명확한 타입 정의 추가:
// 명확한 타입 정의 function findTagBySlug( tags: TagWithCount[], // 입력 타입 명시 slug: string ): TagWithCount | undefined { // 반환 타입 명시 // ... } // 컴포넌트에서 안전한 사용 const tag = findTagBySlug(tags, slug) if (!tag) { notFound() // TypeScript가 이후 tag는 non-null임을 보장 } // 이 시점에서 tag는 확실히 TagWithCount 타입

4.3.3 일관된 링크 생성 패턴

전체 애플리케이션에서 일관된 URL 생성 패턴 적용:
// 일관된 패턴 export const URL_PATTERNS = { tag: (slug: string) => `/tag/${slugify(slug)}`, category: (slug: string) => `/category/${slugify(slug)}`, post: (slug: string) => `/${slug}`, // 포스트는 이미 slugified } as const // 사용 예시 <Link href={URL_PATTERNS.tag(tagName)}> <Link href={URL_PATTERNS.category(categoryName)}>
이러한 패턴을 사용하면:
  • 일관성: 모든 곳에서 동일한 방식으로 URL 생성
  • 유지보수성: URL 패턴 변경 시 한 곳만 수정
  • 타입 안전성: TypeScript에서 잘못된 사용 방지

6. 학습한 내용과 인사이트

6.1 다국어 웹 애플리케이션 고려사항

6.1.1 URL slug 설계 시 다국어 지원 전략

이번 문제를 해결하면서 얻은 다국어 URL 설계에 대한 인사이트:
전략 1: 완전 영문화 (Translation-based)
// 예시: 한글을 영문으로 번역 "홈서버" → "home-server" "개발 블로그" → "development-blog"
  • 장점: URL이 모든 환경에서 안전하게 작동
  • 단점: 번역 품질에 의존, 의미 손실 가능, 번역 DB 유지 필요
전략 2: 로마자 표기법 (Romanization)
// 예시: 한글을 로마자로 변환 "홈서버" → "homeseobeo" "개발 블로그" → "gaebal-beullogeu"
  • 장점: 자동화 가능, 일관된 변환
  • 단점: 가독성 떨어짐, 검색 최적화 어려움
전략 3: 원본 유지 (Native Preservation) ⭐ 선택한 전략
// 예시: 한글을 그대로 유지 "홈서버" → "홈서버" "개발 블로그" → "개발-블로그"
  • 장점: 사용자 친화적, SEO 최적화, 의미 보존
  • 단점: URL 인코딩 처리 복잡성, 브라우저 호환성 고려 필요

6.1.2 사용자 경험과 SEO의 균형점

한글 URL을 선택한 이유와 고려사항:
사용자 경험 측면:
  • 한국어 사용자가 URL만 봐도 내용을 쉽게 파악 가능
  • 소셜 미디어 공유 시 더 직관적이고 신뢰성 있게 보임
  • 브라우저 주소창에서 복사/붙여넣기 시 의미 있는 URL
SEO 측면:
<!-- 한글 URL이 검색 엔진에 제공하는 정보 --> <url> <loc><https://blog.example.com/tag/홈서버></loc> <!-- 검색 엔진이 "홈서버" 키워드와 직접 연관지을 수 있음 --> </url>
  • Google, Naver 등 주요 검색 엔진이 한글 URL을 올바르게 처리
  • 키워드 매칭 시 더 정확한 연관성 파악 가능
  • 사용자가 검색 결과에서 클릭할 가능성 높음

6.1.3 브라우저 호환성 이슈

다양한 브라우저에서의 한글 URL 처리 방식:
최신 브라우저 (Chrome, Firefox, Safari, Edge):
  • 주소창에 한글 표시하되 내부적으로는 URL 인코딩
  • fetch(), XMLHttpRequest 등에서 자동 인코딩/디코딩
  • 복사/붙여넣기 시 한글 형태로 유지
레거시 브라우저 대응:
// 브라우저 호환성을 위한 안전한 처리 function safeDecode(encodedSlug: string): string { try { return decodeURIComponent(encodedSlug) } catch { // 디코딩 실패 시 원본 반환 return encodedSlug } }

6.2 Next.js 최적화 패턴

6.2.1 정적 생성과 동적 라우팅의 조화

이번 해결 과정에서 배운 Next.js SSG + ISR 패턴:
// 빌드 타임에 정적 페이지 생성 export async function generateStaticParams() { const tags = await getAllTags() return tags.map(tag => ({ slug: tag.slug })) } // 런타임에 새로운 태그 처리 export const revalidate = 3600 // 1시간마다 재검증 // 페이지 컴포넌트에서 유연한 매칭 const tag = findTagBySlug(tags, slug) // 여러 방식으로 시도
최적화 포인트:
  • 빌드 타임: 알려진 모든 태그에 대해 정적 페이지 생성
  • 런타임: 새로운 태그나 예외 상황에 대한 유연한 처리
  • 캐싱: ISR로 성능과 최신성 모두 확보

6.2.2 성능을 고려한 fallback 로직

다단계 매칭에서의 성능 최적화 전략:
// 성능 최적화된 매칭 순서 function findTagBySlug(tags: TagWithCount[], slug: string) { // 1. O(n) 선형 검색 - 가장 일반적인 케이스 let tag = tags.find(t => t.slug === slug) if (tag) return tag // 2. 비용이 큰 연산은 필요한 경우에만 try { const decodedSlug = decodeURIComponent(slug) // 비용 있는 연산 // ... 추가 매칭 로직 } catch { // 실패 시 비용 없이 다음 단계로 } }
성능 고려사항:
  • 조기 반환: 대부분의 경우 첫 번째 단계에서 해결
  • 비용 있는 연산 지연: URL 디코딩, slugify 등은 필요 시에만
  • 메모리 효율성: 임시 객체 생성 최소화

6.2.3 타입 안전성을 위한 헬퍼 함수 설계

TypeScript와 함께 사용할 때의 모범 사례:
// 명확한 타입 정의 interface TagFindResult { tag: TagWithCount matchType: 'direct' | 'decoded' | 'normalized' | 'name-based' } function findTagBySlugDetailed( tags: TagWithCount[], slug: string ): TagFindResult | null { // 매칭 방식까지 함께 반환하여 디버깅과 로깅에 활용 } // 사용하는 컴포넌트에서 const result = findTagBySlugDetailed(tags, slug) if (!result) { notFound() } // TypeScript가 이 시점에서 result.tag가 존재함을 보장 console.log(`태그 발견: ${result.tag.name} (${result.matchType})`)
타입 안전성의 이점:
  • 컴파일 타임 검증: 잘못된 속성 접근 방지
  • IDE 지원: 자동완성과 리팩토링 지원
  • 런타임 안정성: null/undefined 체크 강제
Tags:Next.jsError HandlingSEOTILFront-endURL encodingBlogTrouble shooting