현재까지 프로젝트를 진행하며 코드에 주석을 달며, 읽기를 권장드리는 형식으로 설명을 이어왔는데, 이 타이밍에 부족했던 설명들과 브라우저에서의 데이터 통신과 화면 그리기의 방식에 대한 개념을 한 번 상세하게 짚고 넘어가야 할 것 같아 포스팅을 작성하게 되었습니다
app/blog/page.tsx
import { createServerSupabaseClient } from '@/utils/supabase/server';
import Link from 'next/link';
import Image from 'next/image';
/**
* 블로그 목록 페이지 - Server Component
*
* 동기 처리 (Server-side):
* - 페이지 렌더링 전에 모든 포스트 데이터를 서버에서 미리 가져옴
* - SEO 최적화: 검색엔진이 완전한 HTML을 크롤링 가능
* - 초기 로딩 성능: 사용자가 즉시 컨텐츠를 볼 수 있음
* - 안정적인 에러 처리: 서버에서 검증된 데이터만 클라이언트로 전송
*/
export default async function BlogPage() {
// 동기 처리: 서버에서 Supabase 클라이언트 생성
const supabase = await createServerSupabaseClient();
// 동기 처리: 포스트 데이터를 서버에서 미리 페칭
// - 페이지가 렌더링되기 전에 완료되어야 함
// - 필요한 필드만 선택하여 네트워크 효율성 향상
const { data: posts, error } = await supabase
.from("posts")
.select(
"id, title, slug, excerpt, content, featured_image, tags, published_at"
)
.eq("published", true)
.order("published_at", { ascending: false });
if (error) {
console.error("포스트 로딩 오류:", error);
// 동기 처리: 서버에서 에러 상태를 확정하고 에러 UI 반환
// - 클라이언트로 에러가 전파되지 않음
// - 안정적인 사용자 경험 제공
return (
<div className="max-w-6xl mx-auto p-6">
<div className="text-center py-12">
<div className="text-red-400 text-6xl mb-4">⚠️</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
포스트를 불러올 수 없습니다
</h3>
<p className="text-gray-600 mb-6">
잠시 후 다시 시도해주세요.
</p>
<Link
href="/blog"
className="inline-flex px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
다시 시도
</Link>
</div>
</div>
);
}
return (
<div className="max-w-6xl mx-auto p-6">
{/* 헤더 */}
<div className="mb-12 text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
큐라시의 블로그
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Next.js와 Supabase를 사용한 개발 이야기와 기술 인사이트를
공유합니다.
</p>
</div>
{/* 글쓰기 버튼 */}
<div className="mb-8 flex justify-between items-center">
<h2 className="text-2xl font-semibold text-gray-900">
최근 포스트
</h2>
<Link
href="/blog/write"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
새 포스트 작성
</Link>
</div>
{/* 포스트 목록 */}
{/*
동기 처리된 데이터 렌더링:
- 서버에서 이미 가져온 posts 배열을 그대로 렌더링
- 로딩 상태나 스켈레톤 UI가 필요없음 (이미 데이터가 준비됨)
*/}
{posts && posts.length > 0 ? (
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Link
key={post.id}
href={`/blog/${post.slug}`}
className="block"
>
<article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg hover:scale-105 transition-all duration-200 cursor-pointer">
{/* 대표 이미지 */}
{/*
비동기 처리 (클라이언트):
- Next.js Image 컴포넌트가 자동으로 lazy loading 적용
- 이미지는 뷰포트에 들어올 때 비동기로 로드됨
- 사용자 경험 향상을 위한 점진적 로딩
*/}
{post.featured_image && (
<div className="relative h-48 w-full">
<Image
src={post.featured_image}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
<div className="p-6">
{/* 태그들 */}
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{post.tags
.slice(0, 3)
.map(
(
tag: string,
index: number
) => (
<span
key={index}
className="px-2 py-1 text-xs bg-blue-100 text-blue-600 rounded-full"
>
{tag}
</span>
)
)}
</div>
)}
{/* 제목 */}
<h3 className="text-xl font-semibold text-gray-900 mb-2 line-clamp-2 group-hover:text-blue-600 transition-colors">
{post.title}
</h3>
{/* 요약 */}
<p className="text-gray-600 mb-4 mt-3 line-clamp-3">
{post.excerpt ||
post.content
.replace(/<[^>]*>/g, "")
.substring(0, 150) + "..."}
</p>
{/* 메타 정보 */}
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center gap-4">
{/*
향후 비동기 처리가 필요한 기능들:
- 조회수: 사용자가 포스트를 클릭할 때 실시간 증가
- 좋아요: 사용자 인터랙션에 따른 즉시 반영
- 댓글 수: 실시간 업데이트
이런 기능들은 클라이언트 컴포넌트에서 useState, useEffect 사용
*/}
<span className="text-gray-400">
📖 읽기
</span>
</div>
<time
dateTime={
post.published_at || undefined
}
>
{post.published_at &&
new Date(
post.published_at
).toLocaleDateString("ko-KR")}
</time>
</div>
</div>
</article>
</Link>
))}
</div>
) : (
// 동기 처리: 서버에서 이미 posts 배열이 비어있음을 확인하고 렌더링
// 별도의 로딩 상태 관리가 필요없음
<div className="text-center py-12">
<div className="text-gray-400 text-6xl mb-4">📝</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
아직 포스트가 없습니다
</h3>
<p className="text-gray-600 mb-6">
첫 번째 블로그 포스트를 작성해보세요!
</p>
<Link
href="/blog/write"
className="inline-flex px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
첫 포스트 작성하기
</Link>
</div>
)}
</div>
);
}
현재 블로그 포스팅의 목록들을 가져오는 페이지에서 동기적으로 가져오는 부분과 비동기적으로 가져오는 부분에 대해서 주석을 추가해보았습니다. 자세히 들여다 보자면
📝 추가된 주석 요약:
🔄 동기 처리 (Server Component)
- 데이터 페칭: 서버에서 페이지 렌더링 전에 모든 포스트 데이터 미리 로드
- 에러 처리: 서버에서 안정적으로 에러 상태 확정 후 UI 반환
- SEO 최적화: 검색엔진이 완전한 HTML 크롤링 가능
- 초기 성능: 사용자가 즉시 컨텐츠 확인 가능
⚡ 비동기 처리 (현재 및 향후)
- 이미지 로딩: Next.js Image 컴포넌트의 자동 lazy loading
- 향후 기능: 조회수, 좋아요, 댓글 수 등 실시간 인터랙션
🎯 핵심 장점
- 로딩 상태 불필요: 서버에서 이미 데이터가 준비됨
- 안정적인 UX: 에러나 빈 상태도 서버에서 미리 처리
- 점진적 향상: 기본 기능은 동기, 인터랙션은 비동기로 분리
이런 구조로 블로그의 핵심 기능은 안정적으로 동기 처리하고, 사용자 경험을 향상시키는 부분만 비동기로 처리하는 것이 최적의 패턴입니다
자, 그렇다면 동기, 비동기 처리란 무엇이고, 왜 알고 가야하는 개념인가?
🧠 동기 vs 비동기, 그리고 setTimeout과 async는 왜 배우는 걸까?
자바스크립트는 "한 번에 한 줄씩" 코드를 읽고 실행하는 싱글 스레드 언어입니다.
쉽게 말하면, 자바스크립트는 코드를 한 작업이 끝나야 다음 줄의 코드를 처리하는 방식이라는 것입니다
📌 동기(Synchronous)란?
아래 코드를 볼까요?
console.log("1. 밥 준비");
console.log("2. 밥 먹기");
console.log("3. 설거지");
📄 출력:
앞에 있는 작업이 끝나야만 다음 작업을 시작하죠.
이게 바로 "동기 처리"입니다.
⏱ 그런데... 시간이 오래 걸리는 작업이 있다면?
console.log("1. 인터넷으로 영화 다운로드 시작"); // 다운로드가 10분 걸림 😰
console.log("2. 영화 보기");
이런 상황이면 영화 다운로드가 끝날 때까지 이 앱 자체가 멈춘 채로 기다려야 합니다.
그럼 웹사이트는 버벅이고, 멈추고, 사용자도 짜증나겠죠?
그래서 등장한 개념: 비동기 (Asynchronous)
예약 걸어두고, 다른 일 먼저 처리하는 방식!
작업을 “예약해두고” → 나중에 다시 처리하는 거예요.
📡 서버에 데이터 요청할 때도 비동기!
const user = fetch("https://api.example.com/user");
console.log(user.name); // ❌ 아직 데이터 안 왔왔는데 console.log 로 확인을 시도
서버 응답은 시간이 걸리니까, 바로 .name을 꺼내면 오류가 나게 됩니다.
이런 오류나 로딩들을 해결하기 위해 비동기 처리를 아래와 같은 방식으로 구현을 하게 됩니다
1️⃣ Promise와 then 사용하기
fetch("https://api.example.com/user")
.then((response) =>
response.json()
)
.then((data) => {
console.log("유저 이름:", data.name);
}).catch((error) => {
console.error("문제가 생겼어요:", error);
});
- .then() 👉 성공했을 때 실행할 코드
- .catch() 👉 실패했을 때 에러 처리
2️⃣ async / await 방식 (가장 쉽게 읽히는 방법!)
async function getUser() {
try {
const response = await fetch("https://api.example.com/user");
const data = await response.json();
console.log("유저 이름:", data.name);
} catch (error) {
console.error("문제가 생겼어요:", error);
}
}
getUser(); // async await 로 만든 getUser 함수를 실행해봄
🎈 설명:
- async function 👉 이 함수는 비동기적으로 작동
- await 👉 데이터가 올 때까지 기다리고, 다음 줄은 그 이후에 실행
- try 블록 👉 성공한 코드
- catch 블록 👉 실패했을 때 예외 처리
택배가 도착할 때까지 기다렸다가 문을 열고 확인하는 것과 같습니다
🎯 마무리 정리
동기 | 순서대로 처리, 앞이 끝나야 다음 | console.log("1"); console.log("2"); |
비동기 | 느린 작업은 예약하고 나중에 실행 | setTimeout, fetch, async/await |
async/await | 서버 요청 등 비동기 흐름을 읽기 쉽게 처리 | await fetch(...) |
'React' 카테고리의 다른 글
Next.js + Supabase 프로젝트 - (9) : 로그인 회원가입 인증 구현 with Clerk (4) | 2025.07.14 |
---|---|
Next.js + Supabase 프로젝트 - (8) : 포스팅 상세 페이지 구현 (10) | 2025.07.11 |
Next.js + Supabase 프로젝트 - (7) : Supabase CRUD, UI 코드 구현 (해당 파일들 전체 코드 공개) (2) | 2025.07.02 |
Next.js + Supabase 프로젝트 - (6) : 페이지 설계, CRUD 구현 (2) | 2025.07.01 |
Next.js + Supabase 프로젝트 - (5) : Github 연결, vercel 배포, CI/CD 구현하기 (4) | 2025.06.29 |