2026-03-13

개인 블로그에 단축 URL 만들기 — /p/[id]로 한글 URL 문제 해결

문제: 한글 URL은 공유하기 힘들다

블로그 글 URL에 한글 slug를 쓰면 브라우저 주소창에서는 깔끔하게 보인다.

https://zerry.co.kr/blog/pgvector로-블로그-관련-글-추천-구현하기

그런데 이걸 카카오톡이나 슬랙에 붙여넣는 순간, URL 인코딩이 일어난다.

https://zerry.co.kr/blog/pgvector%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A0%A8-%EA%B8%80-%EC%B6%94%EC%B2%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

이건 사람이 읽을 수 있는 URL이 아니다. 받는 사람 입장에서 이게 뭔 링크인지 알 수 없고, 스팸처럼 보일 수도 있다.

해결: 자체 단축 URL

bit.ly 같은 외부 서비스를 쓸 수도 있지만, 개인 블로그에 외부 의존성을 추가하고 싶지 않았다. DB에 이미 글마다 고유한 id가 있으니, 이걸 그대로 쓰면 된다.

https://zerry.co.kr/p/3

어떤 글이든 이 형식이면 끝이다. 짧고, 인코딩 문제도 없다.

구현: Route Handler 하나면 된다

Next.js App Router에서 /p/[id] 경로에 Route Handler를 만든다.

// src/app/p/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { sql } from '@/lib/db';
 
export async function GET(
  _req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const rows = await sql`
    SELECT slug FROM posts WHERE id = ${id} AND status = 'published' LIMIT 1
  `;
 
  if (rows.length === 0) {
    return NextResponse.redirect(new URL('/blog', _req.url), 301);
  }
 
  return NextResponse.redirect(
    new URL(`/blog/${rows[0].slug}`, _req.url),
    301
  );
}

전체 코드가 이게 끝이다. id로 slug를 찾고, 301 리다이렉트를 건다.

왜 301인가

  • 301 (Permanent Redirect): 브라우저가 리다이렉트를 캐시한다. 같은 단축 URL을 두 번째 방문할 때는 서버에 요청하지 않고 바로 이동한다.

  • 302 (Temporary Redirect): 매번 서버를 거친다. 글의 slug가 바뀔 일이 거의 없으니 301이 맞다.

SEO 관점에서도 301은 검색 엔진에 "이 URL의 정식 주소는 저쪽이야"라고 알려준다. 단축 URL이 원본 URL의 검색 순위를 빼앗지 않는다.

존재하지 않는 id는?

삭제된 글이나 없는 id로 접근하면 /blog로 리다이렉트한다. 404를 띄우는 것보다 블로그 메인으로 보내는 게 사용자 경험에 낫다.

공유 버튼에 연결하기

단축 URL을 만들었으면 쓸 곳이 있어야 한다. 글 하단에 공유 버튼을 달았다.

// src/components/blog/ShareButton.tsx
'use client';
 
import { useState } from 'react';
import { Link2, Check } from 'lucide-react';
 
export default function ShareButton({ postId }: { postId: number }) {
  const [copied, setCopied] = useState(false);
 
  const handleCopy = async () => {
    try {
      const shortUrl = `${window.location.origin}/p/${postId}`;
      await navigator.clipboard.writeText(shortUrl);
      setCopied(true);
    } catch {
      setCopied(false);
    }
    setTimeout(() => setCopied(false), 2000);
  };
 
  return (
    <button onClick={handleCopy} aria-label="링크 복사">
      {copied ? <Check /> : <Link2 />}
      {copied ? '복사됨!' : '공유'}
    </button>
  );
}

버튼을 누르면 https://zerry.co.kr/p/3 같은 단축 URL이 클립보드에 복사된다.

왜 외부 서비스를 안 썼나

bit.ly, dub.co 같은 서비스는 잘 만들어져 있다. 하지만 개인 블로그에는 과하다.

  • 외부 서비스가 죽으면 링크가 깨진다

  • 무료 플랜 제한에 걸릴 수 있다

  • 내 도메인이 아닌 URL은 신뢰도가 떨어진다

  • DB에 이미 id가 있는데 왜 별도 서비스를 쓰나

Route Handler 하나, 쿼리 하나, 리다이렉트 하나. 이 정도면 직접 만드는 게 훨씬 간단하다.

결과

Before:

https://zerry.co.kr/blog/nextjs-isr-on-demand-revalidation-%EC%8B%A4%EC%A0%84%EA%B8%B0

After:

https://zerry.co.kr/p/12

공유할 때 깔끔하고, 받는 사람도 부담 없이 클릭할 수 있다. 구현에 걸린 시간은 10분도 안 됐다.

관련 글

벡터 유사도 기반