최적화란?
프론트엔드에서 최적화는 웹 페이지나 애플리케이션의 성능을 개선하기 위해 자원(로딩 시간, CPU 사용량, 메모리 등)을 효율적으로 관리하고, 사용자 경험을 향상시키는 과정입니다.
주로 이미지 압축, 코드 분할, 캐싱, 비동기 로딩 등을 통해 웹 애플리케이션의 속도와 반응성을 높이는 것이 포함됩니다.
목표 (비동기 로딩 - 데이터 패칭)
- Next.js에서 CSR 방식과 SSR 방식을 사용하여 LightHouse 퍼포먼스 성능 비교
- Next.js의 CSR과 SSR 방식에서 LightHouse 성능 점수를 비교하여 각 방식의 성능을 평가합니다.
- CSR 방식과 SSR 방식의 초기 로딩 속도 비교
- CSR 방식과 SSR 방식 각각의 초기 페이지 로딩 시간을 비교하여 어떤 방식이 더 빠른지 분석합니다.
- CSR 방식과 SSR 방식의 실시간 인터랙션 반응 속도 차이 비교
- 사용자가 페이지와 상호작용할 때, CSR과 SSR 방식의 반응 속도 차이를 측정하고 비교합니다.
CSR 코드 기준
//src -> app -> components -> TodoList.tsx
export default function TodoList() {
const [todoList, setTodoList] = useState<TodoListItem[]>([]);
console.log("todoList : ", todoList);
const [completedList, setCompletedList] = useState<TodoListItem[]>([]);
console.log("completedList : ", completedList);
const [notCompletedList, setNotCompletedList] = useState<TodoListItem[]>([]);
console.log("notCompletedList : ", notCompletedList);
const fetchTodoData = async () => {
try {
const res = await fetch("https://assignment-todolist-api.vercel.app/api/junesung/items");
if (!res.ok) {
throw new Error("할 일 목록을 가져오는데 실패했습니다..");
}
const data = await res.json();
const completed = data.filter((todo: TodoListItem) => todo.isCompleted);
const notCompleted = data.filter((todo: TodoListItem) => !todo.isCompleted);
setTodoList(data);
setCompletedList(completed);
setNotCompletedList(notCompleted);
} catch (error) {
console.error("할 일 가져오기 실패 :", error);
}
};
useEffect(() => {
fetchTodoData();
}, []);
return (
.
.
.
)
- 위 코드는 코드잇 심화과정 과제로 했던 내용으로써 CSR 기반으로 작성한 코드입니다.
CSR 초기 렌더링 시간
CSR 기준 첫 데이터 패칭 로드 시간 4.69s 그 이후 452ms초로 평균값이 나옴
CSR LightHouse 성능 분석

※ CSR의 HTML 파일은 빈 화면만 렌더링 하고 있다는 내용을 Preview에서 확인할 수 있다.

- First Contentful Paint
- 첫 번째 콘텐츠가 렌더링된 시점을 측정합니다.
- Largest Contentful Paint
- 페이지 로딩 중 가장 큰 콘텐츠 요소(이미지, 텍스트 블록 등)가 화면에 렌더링 된 시점을 측정합니다. LCP는 사용자에게 가장 중요한 시점으로, 페이지가 얼마나 빨리 완전히 로드되는지를 나타냅니다.
- Total Blocking Time
- 페이지가 사용자와 상호작용할 수 없었던 총 시간을 측정합니다. TBT는 First Interactive부터 Time to Interactive 사이에 자바스크립트 실행이 차단하여 페이지가 반응하지 않는 시간을 합산한 값입니다. 이 시간 동안 사용자 입력이 지연되므로, TBT는 사용자 경험에 중요한 지표입니다.
※ CSR 기준 Performance 성능 점수가 낮게 나오는 이유
- CSR 렌더링 기법은 클라이언트에서 특정 주소로 요청을 하면 서버는 특정 주소에 맞는 빈 HTML 파일을 클라이언트에게 전달한다.
- 그 이후 클라이언트는 브라우저에 빈 HTML화면을 먼저 렌더링 후 JavaScript 파일을 서버에 재 요청을 한다.
- 서버는 해당 주소에 맞는 JavaScript 파일을 클라이언트에게 넘겨주고 클라이언트는 JavaScript 파일을 받은 후 브라우저가 읽을 수 있게 파일을 파싱 후 DOM을 조작해 리렌더링 및 리페인팅 과정을 거쳐 브라우저에 반영한다.
- 이 과정에서 화면이 완전히 로드될 때 까지 기다려야 하는 텀이 존재해 TBT에 성능 점수가 낮다고 개인적으로 생각한다.
- Performance 성능에서 가장 중요하다고 생각하는 부분이 Largest Contentful Paint(메인 콘텐츠) 부분이 가장 빨리 로드되어야 하는데 CSR 기준 메인 콘텐츠를 빨리 가져오지 못하는 부분에서 성능이 낮게 나오는 거라 생각할 수 있다.
※ CSR 기준 SEO 검색엔진 성능이 낮게 나오는 이유
- 위 설명을 토대로 브라우저는 제일 먼저 빈 HTML 파일을 파싱 후 브라우저에 반영하여 렌더링이 된다.
- 이 시점에 검색엔진은 HTML 파일 기준으로 정보를 검색하는데 빈 HTML 파일이라 정보를 얻을 수 없고, 이러한 이유로 SEO 점수가 안 좋을 수밖에 없다고 생각한다.
- 추 후 JavaScript 파일을 받아 DOM을 조작 후 브라우저에 렌더링이 되는 시점에 이미 검색엔진은 해당 페이지를 크롤링했기 때문에, 검색엔진에 반영이 안되고 이러한 종합적인 이유로 SEO 점수가 낮다고 생각한다.
CSR 실시간 인터랙션 반응속도
CSR 기준 첫 인터랙티브 반응속도는 4.5s초가 걸렸고 그 후 450ms초로 비슷하게 측정된다
※ CSR 기준 인터랙티브 반응속도
- CSR 방식에서는 초기 인터랙티브가 활성화되기까지 TBT(Total Blocking Time)가 영향을 미치기 때문에, 페이지가 첫 번째로 상호작용 가능한 상태에 도달하기까지 반응속도가 느리게 측정된 것으로 생각한다.
- 그 이후 인터랙티브 상태로 완전히 활성화된 시점에서는 TBT영향이 없기 때문에 반응속도가 빨라진 거라고 생각한다.
SSR 코드 기준
// src -> app -> api -> todo.ts
import { TodoListItem } from "@/types/todoItemType";
// api/todo.ts (API 레이어 분리)
export const getTodoItems = async (): Promise<TodoListItem[]> => {
const res = await fetch("https://assignment-todolist-api.vercel.app/api/junesung/items", {
cache: "no-cache",
});
return res.json();
};
export const updateTodoStatus = async (id: string, isCompleted: boolean): Promise<TodoListItem> => {
const res = await fetch(`https://assignment-todolist-api.vercel.app/api/junesung/items/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isCompleted }),
});
return res.json();
};
- 데이터를 받아오는 로직을 따로 서버 컴포넌트로 분리하여 작성
"use client";
import { TodoListItem } from "@/types/todoItemType";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { HydrationBoundary, DehydratedState } from "@tanstack/react-query";
import Image from "next/image";
import Link from "next/link";
import { getTodoItems, updateTodoStatus } from "../api/todo";
export default function TodoList({ dehydratedState }: { dehydratedState: DehydratedState }) {
const queryClient = useQueryClient();
// 데이터 조회 쿼리
const { data: todoList } = useQuery<TodoListItem[]>({
queryKey: ["todos"],
queryFn: getTodoItems,
initialData: dehydratedState.queries[0]?.state.data as TodoListItem[], // SSR 초기 데이터
});
// 상태 업데이트 뮤테이션
const { mutate } = useMutation({
mutationFn: (params: { id: string; currentStatus: boolean }) => updateTodoStatus(params.id, !params.currentStatus),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] }); // 데이터 무효화
},
});
// 파생 상태 계산
const completedList = todoList?.filter((todo) => todo.isCompleted) || [];
const notCompletedList = todoList?.filter((todo) => !todo.isCompleted) || [];
return (
<HydrationBoundary state={dehydratedState}>
<article className="flex flex-col md:flex-row gap-8">
{/* TO DO 목록 */}
<section className="flex flex-col gap-5 flex-1">
<Image src="/images/todo.svg" alt="todo-할일 로고" width={101} height={36} priority />
{completedList.length > 0 ? (
completedList.map((todo) => (
<div key={todo.id} className="p-2 bg-[#F9FAFB] flex items-center gap-3 border-[2px] border-black rounded-full">
<Image
src="/images/Property 1=Default.svg"
alt="아이콘"
width={32}
height={32}
priority
className="cursor-pointer"
onClick={() => mutate({ id: todo.id, currentStatus: todo.isCompleted })}
/>
.
.
.
</HydrationBoundary>
);
}
- 위 코드는 코드잇 심화과정 과제로 했던 내용으로써 CSR 기반에서 SSR 코드로 수정한 코드입니다.
SSR 초기 렌더링 시간
SSR 기준 첫 데이터 패칭 로드 시간 918ms 그 이후 939ms초로 평균값이 나옴
※ CSR과 달리 SSR은 모든 데이터가 들어있는 HTML파일을 먼저 로드되기에 브라우저에서는 화면을 더 빨리 보여지는 장점이 있다고 생각한다.
SSR LightHouse 성능 분석

※ Fetch/XHR 탭에서 확인하는게 아닌 Doc에서 확인하는 이유는 데이터가 서버에서 렌더링될 때 클라이언트가 아닌 서버에서 API 요청이 발생하기 때문에 Doc 탭에서 확인을 해야 한다.

※ SSR 기준 SEO 검색엔진 성능이 높게 나오는 이유
- 위 설명을 토대로 서버에서 모든 데이터가 담긴 HTML 파일을 브라우저에 전달한 후 브라우저는 HTML 파일을 파싱 후 화면에 렌더링 하는데 이 시점에 데이터가 들어있어 크롤링 시 검색정보를 얻을 수 있어 SEO 점수가 높게 나온 거라고 예상할 수 있었다.
※ SSR 기준 Performance 성능 점수가 CSR 보다 높게 나오는 이유
- Performance 성능에서 가장 중요하다고 생각하는 건 역시 LCP(Largest Contentful Paint)라고 생각하는데 SSR 기반 데이터를 가져올 때 완성된 HTML 파일을 한 번만 화면에 렌더링 되니 초기 속도가 빨라 Performance 성능이 높게 나온 거라고 추측할 수 있다.
- 하지만 CSR, SSR 기반 성능 분석 결과 TBT가 둘 모두 오래 걸리게 측정이 되었다.
- 보통 SSR이 CSR보다 빨리 걸릴 거라 예상했는데 둘 비슷하게 TBT 시간이 9.8s 혹은 11.2s초 대로 여러 번 측정해도 비슷한 값이 나왔다.
- SSR 기반은 서버에서 데이터를 패치한 후 완성된 HTML 파일을 화면에 한 번만 렌더링 돼서 TBT 시간이 짧을 거라 예상했는데 CSR과 비슷하게 나온 이유를 개인적으로 생각해 보자면 하이드레이션 과정이 오래 걸려서 TBT 시간이 오래 걸렸다고 조심스럽게 생각해 본다.
SSR 실시간 인터랙션 반응속도
SSR 기준 첫 인터랙티브 반응속도는 707ms초가 걸렸고 그 후 610ms초로 비슷하게 측정된다
※ SSR 기준 인터랙티브 반응속도
- 실시간으로 상호작용을 해야 하는 CSR 기준보다 오래 걸릴 거라 예상했지만, 초기 상호작용 시간을 제외하면 CSR, SSR 둘 모두 비슷한 시간으로 작동하는 거를 확인할 수 있었다.
※ 번외 - SSR 기반 코드로 vercel 배포 후 테스트
- 개발 환경에서 성능 분석한 수치와 배포가 완료된 환경에서 분석한 수치가 다르게 나왔다.
- 개발 환경에서 Performance 부분은 80점이 나왔지만, 배포 완료된 환경에서는 100점이 나왔다.
- 배포 완료된 환경에서 Performance 점수가 잘나온 이유를 생각해 보았다
- 빌드 과정에서 코드가 압축되고 코드 스플리팅이 되고, 자원 로딩 시간과 CPU 시간을 최적화해서 Performance 점수가 잘 나온 거라고 조심스레 추측해 본다.
- 배포 완료된 환경에서 Performance 점수가 잘나온 이유를 생각해 보았다
느낀 점
- CSR은 초기 로딩속도가 느린 단점과 SEO에 불리하다고 어떠한 이유 때문인지 알 수 없었으나, 이번 계기로 CSR 구동방식을 공부함으로써 원인을 알 수 있었다
- SSR은 SEO 점수가 높고, 초기 로딩속도가 빠른 이유를 어떤 방식으로 구동되는지 알 수 있었고 정확하게 파악을 할 수 있었다.
- 다만 인터랙티브 부분에서 SSR이 더 빨리 작동할 거라는 예상과 달리, SSR과 CSR 둘 모두 TBT 부분이 오래 걸렸는데, 개인적인 생각이지만 둘 모두 서버를 거쳐 상호작용을 하는 이유 때문에 둘 모두 비슷하게 측정되었다고 조심스레 생각해 볼 수 있었다.
- 추 후에는 CSR, SSR 기반에서 낙관적 업데이트를 적용하여 브라우저에서 먼저 UI 적용시키고 , 그 이후에 서버에서 업데이트하는 방식으로 수정 후 성능 측정을 다시 해보고 싶다고 느꼈다.
'frontend' 카테고리의 다른 글
| [Next.js] - 코드잇(단기 심화) 프로젝트 성능 최적화 (0) | 2025.04.18 |
|---|---|
| [Next.js]렌더링 방식(CSR, SSR, SSG, ISR) - 퍼포먼스 성능 비교 (6) | 2025.01.25 |
| [REACT] 재사용성 컴포넌트 (6) | 2025.01.20 |