์ด๋ฒ ๊ธ์์๋ Next.js ๊ธฐ๋ฐ ํ๋ก์ ํธ์ ํ์ด์ง๋ฅผ ์ฑ๋ฅ ์ต์ ํํ๋ ๊ณผ์ ์ ๊ธฐ๋กํ ๊ฒ์ด๋ค.
๋๋ Google์ Lighthouse ๋๊ตฌ๋ฅผ ์ฌ์ฉํด ์น ์ฑ๋ฅ์ ์ธก์ ํ๊ณ ๊ฐ์ ํด๋ณด์๋ค.
์ฌ์ฉ์๋ ์ฌ์ดํธ๊ฐ ๋๋ฆฐ๊ฑธ ์ซ์ดํ๋ค. ๋ก๋ฉ์ด ์ข๋ง ๊ธธ์ด์ ธ๋ ์ ์์จ. ํ๊ณ ๋๊ฐ
์ฑ๋ฅ์ด ๋ฎ์ผ๋ฉด ์ฌ์ฉ์ ์ดํ๋ฅ ์ด ๋์์ง๊ณ , ๊ฒ์์์ง ์ต์ ํ(SEO)์๋ ๋ถ๋ฆฌํ๋ค.
ํนํ Next.js๋ SSR์ ์ง์ํ์ง๋ง, ์๋ชป์ฐ๋ฉด ์คํ๋ ค ๋๋ ค์ง ์ ์๋ค.
๊ทธ๋์ ๋๋ Lighthouse๋ฅผ ํตํด ์ฑ๋ฅ ๋ณ๋ชฉ์ ํ์ธํ๊ณ ์ง์ ๊ฐ์ ํด๋ณด๊ธฐ๋ก ํ๋ค.
Lighthouse๋ Chrome ๋ธ๋ผ์ฐ์ ์์ ์ ๊ณตํ๋ ์น์ฌ์ดํธ ํ์ง ๋ถ์ ๋๊ตฌ์ด๋ค.
์ฑ๋ฅ ์ ์๋ถํฐ ์ ๊ทผ์ฑ, SEO, Best Practice๊น์ง ์์์ ๋ถ์ํด์ค๋ค.
f12 โ Lighthouse ํญ โ "ํ์ด์ง ๋ก๋ ๋ถ์" ํด๋ฆญํ๋ฉด ์ธก์ ๋!
์งํ | ์ค๋ช |
---|---|
LCP (Largest Contentful Paint) | ๊ฐ์ฅ ํฐ ์ฝํ ์ธ ๊ฐ ํ๋ฉด์ ๋ ๋๋ง๋ ๋๊น์ง ๊ฑธ๋ฆฐ ์๊ฐ |
CLS (Cumulative Layout Shift) | ํ๋ฉด ์์๊ฐ ๊ฐ์๊ธฐ ์์ง์ด๋ ์ ๋ |
TBT (Total Blocking Time) | ๋ฉ์ธ ์ค๋ ๋๊ฐ ๋งํ ์ฌ์ฉ์๊ฐ ์๋ฌด๊ฒ๋ ํ ์ ์๋ ์๊ฐ |
TTFB (Time To First Byte) | ์ฒซ ๋ฐ์ดํธ๊ฐ ๋ธ๋ผ์ฐ์ ์ ๋๋ฌํ๋ ์๊ฐ (์๋ฒ ์๋ต ์๋) |
๐ /study
(์ฒซ ํ์ด์ง) ์ง์
ํ ๋ ๋๋ฌด ๋๋ ธ์. ์ธ๊ธฐ๊ธ + ์ ์ฒด ๊ฒ์๊ธ ํ๊บผ๋ฒ์ ๋ถ๋ฌ์ค๋ฉด์, ์ค์ํ ์ฝํ
์ธ ๊ฐ ๋ฆ๊ฒ ๋จ๋ ๋ฌธ์ ๊ฐ ์์์
โ Minimize main-thread work : use client
๋ค์ด์ดํธ
use client
๊ฐ ๋ถ์ ์ปดํฌ๋ํธ ์ค์์ ์ ์ UI๋ง ์ฒ๋ฆฌํ๋ ์ปดํฌ๋ํธ๋ ์๋ฒ ์ปดํฌ๋ํธ๋ก ์ ํํด ๋ฉ์ธ ์ค๋ ๋์ ๋ถ๋ด์ ์ค์ฌ๋ณด์.
import React from "react";
import { formatRelativeTime } from "@/lib/date";
import { Study, User } from "@prisma/client";
import Image from "next/image";
import { FaBookmark } from "react-icons/fa";
import Link from "next/link";
type StudyType = Study & {
author: User;
comments: Comment[];
};
interface Props {
study: StudyType;
isLast: boolean;
observerRef: React.RefObject<HTMLDivElement | null>;
}
const StudyCard = ({ study, isLast, observerRef }: Props) => {
const today = new Date();
today.setHours(0, 0, 0, 0); // ์ค๋ 00:00:00
return (
<Link href={`/study/${study.id}`} key={study.id}>
<div
ref={isLast ? observerRef : null}
className=" min-h-[200px] group bg-white rounded shadow-sm border border-gray-200 p-6 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
>
<div className="space-y-4">
<div className="flex items-start gap-5">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-200">
{study.category}
</span>
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
study.startDate && new Date(study.startDate) < today
? "bg-gray-300 text-gray-500"
: "bg-green-200 text-green-700"
}`}
>
{study.startDate && new Date(study.startDate) < today
? "๋ชจ์ง ๋ง๊ฐ"
: "๋ชจ์ง์ค"}
</span>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900 transition-colors line-clamp-1">
{study.title}
</h3>
<p
className="text-sm text-gray-600 line-clamp-2 leading-relaxed
min-h-[48px]"
>
{study.content.replace(/[#_*~`>[\]()\-!\n]/g, "").slice(0, 100)}
</p>
</div>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{formatRelativeTime(new Date(study.createdAt))}</span>
<span>โข</span>
<span>{study._count.comments}๊ฐ์ ๋๊ธ</span>
</div>
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
<div className="flex items-center gap-3">
<div>
<div className="w-8 h-8 rounded-full overflow-hidden relative">
<Image
src={study.author.profileImage ?? "/default-avatar.png"}
alt="ํ๋กํ"
fill
className="object-cover"
/>
</div>
</div>
<span className="text-sm text-gray-700 font-medium ">
{study.author.nickname}
</span>
</div>
<div className="flex items-center gap-1 text-red-500">
<FaBookmark className="text-sm" />
<span className="text-sm font-medium">{study.scrap}</span>
</div>
</div>
</div>
</div>
</Link>
);
};
export default StudyCard;
๊ทธ๋ฆฌ๊ณ StudyList.tsx
์์๋ ์ด๋ ๊ฒ ์ฌ์ฉ
import StudyCard from "./StudyCard";
// ...
{studies.map((study, index) => (
<StudyCard
key={study.id}
study={study}
isLast={index === studies.length - 1}
observerRef={observerRef}
/>
))}
์ฑ๋ฅ ์ ์๋ ๊ทธ๋๋ก์์ง๋ง, ๊ธธ์๋ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํด์ ๊ฐ๋ ์ฑ์ด ์ข์์ก๊ณ , ์ ์ง๋ณด์์ฑ ํฅ์
์ง์ฐ ๋ก๋ฉ, DB ์ฟผ๋ฆฌ ์ต์ ํ, dynamic import ์จ์
์ ์๊ฐ ์ง์ง๋ก ์ค๋ฅด๊ธฐ ์์ํ ์ด์ผ๊ธฐ๋ ๋ค์ ํธ์์