본문 바로가기
React

Next.js + Supabase 프로젝트 - (9) - 2 : 추가 설명 포스팅 - js 동기, 비동기 처리란

by Junmannn 2025. 7. 17.
반응형

현재까지 프로젝트를 진행하며 코드에 주석을 달며, 읽기를 권장드리는 형식으로 설명을 이어왔는데, 이 타이밍에 부족했던 설명들과 브라우저에서의 데이터 통신과 화면 그리기의 방식에 대한 개념을 한 번 상세하게 짚고 넘어가야 할 것 같아 포스팅을 작성하게 되었습니다

 

 

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
  • 향후 기능: 조회수, 좋아요, 댓글 수 등 실시간 인터랙션

🎯 핵심 장점

  1. 로딩 상태 불필요: 서버에서 이미 데이터가 준비됨
  2. 안정적인 UX: 에러나 빈 상태도 서버에서 미리 처리
  3. 점진적 향상: 기본 기능은 동기, 인터랙션은 비동기로 분리

이런 구조로 블로그의 핵심 기능은 안정적으로 동기 처리하고, 사용자 경험을 향상시키는 부분만 비동기로 처리하는 것이 최적의 패턴입니다

 

 

자, 그렇다면 동기, 비동기 처리란 무엇이고, 왜 알고 가야하는 개념인가?

 

🧠 동기 vs 비동기, 그리고 setTimeout과 async는 왜 배우는 걸까?

자바스크립트는 "한 번에 한 줄씩" 코드를 읽고 실행하는 싱글 스레드 언어입니다.

쉽게 말하면, 자바스크립트는 코드를 한 작업이 끝나야 다음 줄의 코드를 처리하는 방식이라는 것입니다


📌 동기(Synchronous)란?

아래 코드를 볼까요?

console.log("1. 밥 준비");
console.log("2. 밥 먹기");
console.log("3. 설거지");

 

📄 출력:

1. 밥 준비
2. 밥 먹기
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 사용하기

js
CopyEdit
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(...)

 

반응형