sooyoung archive 제작기
나만의 블로그 만들기: Next.js + Supabase로 개인 아카이브 구축하기
시작하며
개인 블로그를 만들고 싶다는 생각이 들었어요. 단순히 글만 쓰는 게 아니라, 나만의 공간을 만들어보고 싶었습니다. 그래서 시작한 프로젝트가 Sooyoung Archive입니다.
이 글에서는 처음부터 끝까지 블로그를 만드는 과정을 정리해봤어요. 기술 스택은 Next.js, TypeScript, Supabase, NextAuth를 사용했습니다.
1. 프로젝트 초기 설정
기술 스택 선택
- Next.js 16 (App Router)
- TypeScript
- Tailwind CSS
- Supabase (PostgreSQL + Storage)
- NextAuth (GitHub 소셜 로그인)
- React Markdown (마크다운 렌더링)
기본 구조
sooyoung-archive/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # 홈 (글 목록)
│ │ ├── post/[slug]/ # 글 상세
│ │ ├── write/ # 글 작성/수정
│ │ └── login/ # 로그인 페이지
│ ├── components/ # 재사용 컴포넌트
│ ├── lib/ # 유틸리티 함수
│ └── types/ # TypeScript 타입
└── data/
└── posts.json # 초기 데이터 (로컬)
2. 핵심 기능 구현
2.1 이미지 URL 자동 변환
마크다운 에디터에서 이미지 URL을 붙여넣으면 자동으로 마크다운 이미지 형식으로 변환하는 기능을 만들었어요.
// src/lib/markdown.ts
const IMAGE_URL_LINE =
/^\s*(https?:\/\/\S+|\/\S+)\.(?:png|jpe?g|gif|webp|svg)(?:\?\S*)?\s*$/i;
export function normalizeMarkdownImages(markdown: string) {
if (!markdown.trim()) return markdown;
return markdown
.split("\n")
.map((line) => {
const trimmed = line.trim();
if (!trimmed) return line;
if (!IMAGE_URL_LINE.test(trimmed)) return line;
return ``;
})
.join("\n");
}
이제 https://example.com/image.jpg 같은 URL을 한 줄로 붙여넣으면 자동으로 로 변환돼요.
2.2 슬러그 생성 및 매칭 로직
한글 제목을 URL 친화적인 슬러그로 변환하고, 기존 잘못된 슬러그도 찾을 수 있도록 했어요.
function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^0-9a-z가-힣-]+/g, "");
}
function normalizeSlug(value: string): string {
try {
return decodeURIComponent(value).normalize("NFC").toLowerCase();
} catch {
return value.normalize("NFC").toLowerCase();
}
}
문제 해결: 처음에 슬러그 생성 로직이 잘못되어 -가 같은 이상한 슬러그가 생성되었어요. 정규식을 수정하고, 기존 데이터도 호환되도록 legacy 매칭 로직을 추가했어요.
2.3 글 수정/삭제 기능
상세 페이지에서 관리자만 수정/삭제 버튼을 볼 수 있도록 했어요.
// src/app/post/[slug]/post-actions.tsx
export function PostActions({ slug }: PostActionsProps) {
const router = useRouter();
const [deleting, setDeleting] = useState(false);
async function handleDelete() {
if (!confirm("정말 삭제할까요? 이 작업은 되돌릴 수 없습니다.")) return;
// 삭제 로직...
}
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" asChild>
<Link href={`/write/${slug}`}>수정</Link>
</Button>
<Button variant="destructive" onClick={handleDelete}>
삭제
</Button>
</div>
);
}
2.4 반응형 레이아웃 통일
모든 페이지가 일관된 너비와 가운데 정렬을 갖도록 했어요.
// 모든 페이지에서 동일한 컨테이너 클래스 사용
<main className="mx-auto w-full max-w-7xl px-4 py-8">
문제 해결: 처음에는 container 클래스를 사용했는데, 화면이 넓을 때 왼쪽으로 치우쳐 보였어요. mx-auto w-full max-w-7xl로 통일해서 해결했어요.
3. 디자인 시스템
3.1 키 컬러 정의
"수영"이라는 이름에서 영감을 받아 수영장 컨셉의 컬러를 선택했어요.
/* src/app/globals.css */
:root {
--primary: #009ec2; /* Deep Ocean Blue */
--accent: #c9f2ff; /* Light Cyan */
--background: #f8fafc; /* Slate White */
--foreground: #0c0c0c; /* Rich Black */
}
.dark {
--background: #0c0c0c;
--foreground: #f8fafc;
/* ... */
}
3.2 로고 디자인
"sooyoung archive"의 중의적 의미(수영장 + 아카이브)를 담은 로고를 만들었어요.
- 물결 아이콘: 수영장의 물결
- 워드마크:
sooyoung(검은색) +archive(파란색)
<Link href="/" className="flex items-center gap-2">
<span className="inline-flex h-6 w-6 items-center justify-center text-primary">
{/* 물결 SVG 아이콘 */}
</span>
<span className="text-base font-semibold">
sooyoung<span className="text-primary">archive</span>
</span>
</Link>
4. 인증 시스템
4.1 GitHub 소셜 로그인
NextAuth를 사용해서 GitHub 로그인을 구현했어요.
// src/auth.ts
export const authOptions: NextAuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.AUTH_GITHUB_ID ?? "",
clientSecret: process.env.AUTH_GITHUB_SECRET ?? "",
}),
],
session: { strategy: "jwt" },
callbacks: {
async jwt({ token, profile }) {
if (profile && "login" in profile) {
token.login = String(profile.login);
}
return token;
},
async session({ session, token }) {
// 관리자 여부를 세션에 추가
(session as { isAdmin?: boolean }).isAdmin = isAdminSession(session);
return session;
},
},
};
4.2 관리자 권한 체크
환경 변수에 설정한 GitHub 계정만 글 작성/수정/삭제가 가능하도록 했어요.
// src/lib/auth.ts
const ADMIN_EMAIL = process.env.ADMIN_EMAIL?.toLowerCase();
const ADMIN_GITHUB = process.env.ADMIN_GITHUB?.toLowerCase();
export function isAdminSession(session: Session | null) {
if (!session?.user) return false;
const email = session.user.email?.toLowerCase();
const login = session.user.login?.toLowerCase();
if (ADMIN_EMAIL && email && email === ADMIN_EMAIL) return true;
if (ADMIN_GITHUB && login && login === ADMIN_GITHUB) return true;
return false;
}
권한 구조:
- 공개: 글 목록 보기, 글 상세 보기
- 로그인 필요: 댓글 작성 (향후 추가 예정)
- 관리자만: 글 작성/수정/삭제, 이미지 업로드
5. 데이터베이스 연동
5.1 Supabase 설정
로컬 파일 저장소(data/posts.json)에서 Supabase로 마이그레이션했어요.
// src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
5.2 데이터베이스 스키마
-- supabase-schema.sql
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
excerpt TEXT,
thumbnail TEXT,
body TEXT NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts("createdAt" DESC);
5.3 이미지 업로드 (Supabase Storage)
로컬 파일 시스템 대신 Supabase Storage를 사용하도록 변경했어요.
// src/app/api/upload/route.ts
const { data, error } = await supabase.storage
.from("uploads")
.upload(filename, buffer, {
contentType: file.type,
upsert: false,
});
const {
data: { publicUrl },
} = supabase.storage.from("uploads").getPublicUrl(filename);
6. 주요 문제 해결 과정
문제 1: 슬러그 생성 오류
증상: -가 같은 이상한 슬러그가 생성됨
원인: 정규식에서 -가 범위로 해석됨
해결: [^0-9a-z가-힣-]+로 수정하고, 기존 데이터는 legacy 매칭으로 호환
문제 2: 이미지 URL이 마크다운으로 변환 안 됨
증상: 이미지 URL을 붙여넣어도 이미지로 표시 안 됨
해결: normalizeMarkdownImages 함수로 URL을 자동 감지해서 마크다운 형식으로 변환
문제 3: 레이아웃이 왼쪽으로 치우침
증상: 화면이 넓을 때 콘텐츠가 왼쪽에 몰림
해결: container 대신 mx-auto w-full max-w-7xl로 통일
문제 4: 관리자 계정 매칭 실패
증상: 로그인은 되는데 작성 버튼이 안 보임
해결: 세션에 login 필드 추가하고, 환경 변수와 정확히 매칭하도록 수정
7. 배포 준비
환경 변수 설정
# GitHub OAuth
AUTH_GITHUB_ID=your_github_client_id
AUTH_GITHUB_SECRET=your_github_client_secret
# 관리자 계정
ADMIN_EMAIL=your_email@example.com
ADMIN_GITHUB=your_github_username
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
배포 체크리스트
- [x] Supabase 프로젝트 생성 및 스키마 실행
- [x] Storage 버킷 생성 (
uploads) - [x] 환경 변수 설정
- [x] GitHub OAuth 앱 생성
- [ ] Vercel 등에 배포
- [ ] 도메인 연결 (선택사항)
8. 향후 계획
- 댓글 기능: 로그인한 사용자가 댓글 작성 가능
- 태그 시스템: 글에 태그 추가 및 태그별 필터링
- 검색 기능: 제목/본문 검색
- 다크모드: 이미 준비되어 있음 (CSS 변수 정의됨)
- RSS 피드: 블로그 구독 기능
마무리
처음에는 단순히 글만 쓰는 블로그를 만들려고 했는데, 만들다 보니 정말 나만의 공간이 되었어요.
특히 **"수영"**이라는 이름에서 영감을 받아 수영장 컨셉의 디자인을 적용한 게 마음에 들어요. 물결 아이콘과 블루 컬러가 블로그의 정체성을 잘 표현한다고 생각해요.
기술적으로는 Next.js의 App Router와 Supabase의 조합이 정말 좋았어요. 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 섞어서 성능도 좋고 개발 경험도 좋았습니다.
앞으로도 이 공간에 제 생각과 경험을 기록해나가려고 해요. 🏊♀️
작성일: 2026년 2월 2일
기술 스택: Next.js 16, TypeScript, Supabase, NextAuth, Tailwind CSS