나의 첫 개발블로그
안녕하세요 저는 프론트엔드 개발자 박찬정입니다.
일단 제가 개발블로그를 만들었던 과정을 차근차근 이야기로 풀어내려고 합니다.
개발 블로그를 만들게 된 이유?
누구나 다 알듯이 velog나 tistory가 제가 만든 것보다 더 깔끔하고 편합니다. 기능적으로도 크게 다른 점이 없어서, 굳이 왜 만들었지? 라고 물어보실 수 있는데 제가 블로그를 만든 이유는 나만의 개인공간을 만들고 싶어서입니다. velog나 tistory는 보안도 훨씬 더 잘되있고, 많은 개발자들이 사용하기 때문에 소통도 잘 됩니다. 하지만 velog나 tistory가 언젠가 사라진다면? 그럴 일은 희박하겠지만, 저만의 공간이 아닌 거 같다라는 생각이 들었습니다. 그래서 단순하지만 좋은 개발블로그 페이지를 만든다면 두고두고 사용할 수 있지 않을까? 라는 생각에 시작을 하게 되었습니다. 물론 공부 목적도 있구요.
어떻게 만들었죠?
저는 프론트엔드 개발자이기 때문에, 백엔드는 최대한 단순하게 만들고 싶었습니다
각각의 기술스택을 어떻게 활용했는지 간단하게 소개하겠습니다.
🎨 Frontend
Next.js 15.5.2 - React 기반의 풀스택 프레임워크로, 서버사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 지원
React 19.1.0 - 사용자 인터페이스 구축을 위한 라이브러리
TypeScript 5 - JavaScript에 정적 타입 시스템을 추가하여 코드의 안정성과 개발 생산성 향상
Tailwind CSS 4 - 유틸리티 퍼스트 CSS 프레임워크로 빠른 스타일링
shadcn/ui - Radix UI 기반의 재사용 가능한 컴포넌트 라이브러리
Radix UI 컴포넌트들 (Dialog, Label, Navigation Menu, Switch 등)
Tiptap 3.4.4 - 리치 텍스트 에디터 (블로그 포스트 작성용)
React Markdown - 마크다운 콘텐츠 렌더링
Next Themes - 다크/라이트 모드 테마 전환
Lucide React - 아이콘 라이브러리
Heroicons - 추가 아이콘 라이브러리
🔧 Backend
Next.js API Routes - 서버사이드 API 엔드포인트
Prisma 6.15.0 - 데이터베이스 ORM (Object-Relational Mapping)
PostgreSQL - 관계형 데이터베이스 (Prisma를 통해 연결)
Supabase - 백엔드 서비스 (파일 스토리지, 인증 등)
JWT (jsonwebtoken) - 관리자 인증을 위한 토큰 기반 인증
bcryptjs - 비밀번호 해싱
Server Actions - Next.js의 서버사이드 액션 (폼 처리 등)
🚀 CI/CD & 배포
ESLint 9 - 코드 품질 관리 및 린팅
Prettier 3.6.2 - 코드 포맷팅
PostCSS - CSS 후처리
Vercel (추정) - Next.js 프로젝트의 일반적인 배포 플랫폼
프로젝트 시작: "이거 진짜 할 수 있나?"
처음 Next.js 15와 TypeScript로 블로그를 만들겠다고 마음먹었던 동기는 제가 인턴생활하면서 만났던 동료들 때문인데요, 솔직히 말하면 "이거 진짜 할 수 있나?" 하는 생각이 먼저 들었습니다. 왜냐하면, 게시글들의 업데이트 상황을 빨리 처리가능할까? 그럼 이 게시글들은 안전하게 로컬에다가 보관해야 될까? 아니면 서버? 서버는 돈이 들지 않을까? 생각이 들었습니다.
그럼 이제 본격적으로 제가 겪었던 시행착오들을 풀어드리겠습니다
첫 번째 시행착오: 에디터 선택의 고민
// 처음에는 이런 식으로 간단하게 시작했었는데...
<textarea
className="w-full h-96 p-4 border rounded-lg"
placeholder="내용을 입력하세요..."
/>하지만 이건 너무 밋밋하더라고요. 코드 하이라이팅도 되고, 이미지도 넣을 수 있고, 링크도 걸 수 있는 진짜 에디터가 필요했어요. 그래서 Tiptap을 선택했는데, 이게 또 하나의 시행착오의 시작이었습니다.
Tiptap을 처음 구현했을 때 가장 큰 문제는 SSR Hydration 에러였습니다.
const editor = useEditor({
extensions: [StarterKit],
content: initialContent,
// immediatelyRender: true (기본값)
});문제점:
서버에서 렌더링된 HTML과 클라이언트에서 렌더링된 HTML이 달라서 에러 발생
suppressHydrationWarning을 써도 근본적인 해결책이 아님
해결책:
// 1. 클라이언트에서만 마운트
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 2. immediatelyRender: false 설정
const editor = useEditor({
extensions: [StarterKit],
content,
immediatelyRender: false, // 이게 핵심!
});
// 3. 마운트 전까지 로딩 표시
if (!mounted || !editor) {
return <div>에디터를 로딩 중...</div>;
}이 문제를 해결하는데만 3일이 걸렸습니다. 정말 "왜 안 돼?"를 외치며 밤새 코드를 뒤적거렸습니다.
하지만 결국 DONE
두번째 시행착오 - 파일 스토리지 선택의 고민
에디터에 이미지를 넣을 수 있게 하려면 파일 업로드 기능이 필요했습니다. 처음에는 AWS S3를 고려했는데, 설정이 너무 복잡해 보여서 Supabase Storage로 갈아탔습니다.
문제 1: 파일 크기 제한
// 10MB 제한을 두고, 5MB 이상이면 압축
const maxSizeBytes = 10 * 1024 * 1024;
const compressThresholdBytes = 5 * 1024 * 1024;
const fileToUpload = file.size > compressThresholdBytes
? await compressImage(file)
: file;문제 2: 드래그 앤 드롭 구현
// 에디터에서 드래그 앤 드롭 처리
handleDrop: (view, event) => {
const files = event.dataTransfer?.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
event.preventDefault();
handleImageUpload(file);
return true;
}
}
return false;
},이미지 업로드 기능 하나 구현하는데 일주일이 걸렸어요. 특히 RLS 정책 설정하는 부분에서 ai도 모르고 stackoverflow도 원하는 대답이 나오지 않아서 정말 모르다가 어느날 ai가 제안한 방법이 성공했고 다행히 storage를 만들어서 저장할 수 있었습니다.
3번째 시행착오 - 관리자 로그인을 어떻게 만들지?
블로그에 관리자 기능이 필요했는데, 사용자 인증 시스템을 처음부터 만들기엔 너무 복잡해 보였어요. 그래서 JWT + HttpOnly 쿠키 방식으로 간단하게 구현했습니다.
// 로그인 액션
export async function loginAction(prevState: LoginState, formData: FormData) {
const password = formData.get('password') as string;
const secretKey = formData.get('secretKey') as string;
// 비밀키 검증
if (secretKey !== process.env.ADMIN_SECRET_KEY) {
return { success: false, error: '비밀키가 올바르지 않습니다.' };
}
// 비밀번호 확인
const isValidPassword = await bcrypt.compare(password, admin.password);
if (!isValidPassword) {
return { success: false, error: '비밀번호가 올바르지 않습니다.' };
}
// JWT 토큰 생성
const token = jwt.sign(
{ adminId: admin.id, timestamp: Date.now(), role: 'admin' },
process.env.JWT_SECRET!,
{ expiresIn: '1h' }
);
// HttpOnly 쿠키로 설정
(await cookies()).set('admin-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600,
});
}처음에는 middleware에서 JWT를 검증하려고 했는데, 서버 컴포넌트와 클라이언트 컴포넌트의 경계에서 문제가 생겼어요. 결국 middleware에서는 토큰 존재 여부만 확인하고, 실제 검증은 각 페이지에서 하도록 바꿨어요.
마무리: 한 달의 여정을 돌아보며
한 달 동안 정말 스트레스도 받고 "희노애락"이 많았던 거 같습니다. 특히 Next.js 15의 새로운 기능들과 TypeScript의 타입 시스템, Prisma의 편리함, Supabase의 간편함 등을 경험할 수 있었습니다.
앞으로의 계획:
관리자 페이지 최적화
검색 기능 구현
SEO 최적화
도메인 구매(chanjeongpark.com)
에디터 성능 최적화