나의 첫 개발블로그

2025년 9월 29일

안녕하세요 저는 프론트엔드 개발자 박찬정입니다.

일단 제가 개발블로그를 만들었던 과정을 차근차근 이야기로 풀어내려고 합니다.

개발 블로그를 만들게 된 이유?

누구나 다 알듯이 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)

  • 에디터 성능 최적화