여행 가이드 관련 프론트엔드 외주 프로젝트를 맡게 되었고, 아래와 같은 요구 사항이 있었다. 특히 여행 관련 컨텐츠가 주를 이루기 때문에, 여행지에 대한 검색 유입이 가장 중요할 것이다. 그래서 클라이언트는 SEO에 힘을 써달라고 요구했다.
💡 SSR + CSR 로 구성해주시고, 페이지마다 검색을 통해 들어올 수 있도록 주요 컨텐츠는 SSR로 보여주세요.
이번 외주 프로젝트를 통해서 SEO를 염두하며 SSR과 CSR을 어떻게 나눌지에 대한 고민을 주로 했던 것 같다. 해당 포스트에서는 내가 고민하고 학습했던 Next.js14의 SSR, CSR에 대해서 주로 다룰 예정이다.
💡 Next.js는 정적 사이트 생성(SSG), 서버 사이드 렌더링(SSR), 증분 정적 재생성(ISR)을 가능하게 하는 React 프레임워크이다. 성능, 확장성, 사용의 용이성으로 잘 알려져 있다.
Next.js 14의 도입으로 서버 액션이 안정화되었다. 그러나 서버 액션이란 무엇이며, 외부 API에서 데이터를 가져오고 업데이트하는 데 어떻게 사용할 수 있을까?
또한 Next.js 14에서는 앱 라우터(App Router)를 통하여 getServerSideProps와 같은 방식 대신, 서버 컴포넌트와 클라이언트 컴포넌트를 나누어 SSR과 CSR을 구현할 수 있다. 어떻게 더 직관적이고 효율적으로 서버 사이드 렌더링과 클라이언트 사이드 렌더링을 관리할 수 있을까?
서버 액션은 Next.js 13부터 존재했지만, 14 버전에서는 서버 액션이 안정화되어 기본적으로 프레임워크 기능에 통합되었다.
서버 액션은 서버에서 데이터를 처리하고 클라이언트에 전달하는 과정을 간소화한다. 이는 별도의 API 라우트를 생성하지 않고도 서버 측 코드를 실행할 수 있게 해준다. 서버 액션은 보안이 중요한 작업이나 서버에서만 처리 가능한 로직을 포함할 때 유용하다.
서버에서만 처리 가능한 로직이란 보안상 클라이언트에서 실행해서는 안 되거나, 클라이언트에서 실행할 수 없는 작업을 의미한다. 예를 들어, 데이터베이스 쿼리, 외부 API 호출 시 인증 키 사용, 파일 시스템 접근, 비즈니스 로직 처리 등이 이에 해당한다. 이러한 작업은 보안 문제를 피하기 위해 반드시 서버에서 실행되어야 한다.
SSR(Server-Side Rendering)은 사용자가 페이지를 요청할 때 서버에서 HTML을 생성하여 응답하는 방식이다. 이 HTML은 클라이언트에 완전한 형태로 전달되므로, 사용자는 초기 로딩 시 완성된 페이지를 볼 수 있다.
검색 엔진은 서버에서 렌더링된 완전한 HTML 페이지를 쉽게 크롤링할 수 있어, 페이지의 검색 엔진 순위가 올라간다. 즉, SEO에 좋다. 그리고 클라이언트는 완성된 HTML을 받기 때문에 초기 로딩 속도가 빨라 사용자 경험에 좋은 영향을 미친다.
또한 SSR을 통해 각 페이지에 고유한 메타 태그를 설정할 수 있어, 검색 엔진 최적화에 큰 도움이 된다.
각 요청마다 서버에서 HTML을 생성하므로, 서버 자원이 많이 소모된다. 특히 트래픽이 많은 사이트에서는 서버 부하가 크게 증가할 수 있다.
또한 서버에서 HTML을 생성하는 데 시간이 소요되므로, 클라이언트 응답 시간이 길어질 수 있다. 이는 특히 서버가 과부하 상태일 때 문제가 될 수 있다.
그리고 클라이언트 측에서 실행되는 동적 기능이 제한될 수 있다. 클라이언트 측에서 인터랙티브한 기능을 구현하려면 추가적인 작업이 필요하다.
CSR(Client-Side Rendering)은 페이지가 로드된 후 클라이언트 측에서 JavaScript를 통해 데이터를 가져와서 화면을 렌더링하는 방식이다. 초기에는 최소한의 HTML만 제공되고, 나머지 내용은 JavaScript가 실행되면서 동적으로 채워진다.
클라이언트 측에서 라우팅을 처리하므로, 페이지 전환 시 전체 페이지를 다시 로드할 필요가 없다. 이는 더 빠른 페이지 전환을 가능하게 하고, 사용자 경험을 향상시킨다. 즉, 사용자가 상호작용하는 동안 페이지의 일부만 업데이트할 수 있어 더 동적이고 반응성이 뛰어난 사용자 인터페이스를 제공한다. 예를 들어 페이지 전체를 다시 로드하지 않고도, 특정 컴포넌트의 상태를 실시간으로 업데이트할 수 있다.
또한, 서버에서 초기 요청에 최소한의 HTML만 제공하므로, 서버 부하를 줄일 수 있다. 이는 트래픽이 많은 애플리케이션에서 특히 유용하다.
하지만 초기 로딩 시 최소한의 HTML만 제공하기 때문에, 검색 엔진이 페이지를 제대로 크롤링하지 못할 수 있다. 이는 SEO에 불리할 수 있다.
그리고 초기 로딩 시 모든 JavaScript를 다운로드하고 실행해야 하기 때문에, 첫 번째 페인트(FCP, First Contentful Paint)가 지연될 수 있다.
💡 첫 번째 페인트(FCP, First Contentful Paint)
웹 페이지가 로드되기 시작할 때, 브라우저가 첫 번째로 화면에 유의미한 콘텐츠를 렌더링하는 시점을 의미한다. FCP는 사용자가 실제로 볼 수 있는 첫 번째 콘텐츠가 화면에 나타나는 시간을 측정하는 웹 성능 지표이다.
서버 액션(Server Actions)과 서버 사이드 렌더링(SSR)은 관련이 있지만 동일한 개념은 아니다. 각각의 역할과 의미가 다르다. 서버 액션은 서버에서 데이터 처리 작업을 수행하는 기능이며, SSR은 서버에서 HTML을 생성하여 클라이언트에 전달하는 렌더링 방식이다. 즉, 서버 액션은 SSR의 일부로 사용될 수 있지만, 서버 액션 자체가 SSR을 의미하는 것은 아니다.
Next.js 14에서는 앱 라우터(App Router)를 통해 서버 컴포넌트와 클라이언트 컴포넌트를 결합하여 위에서 설명한 SSR과 CSR을 간단하게 구현할 수 있다. 이를 통해 각 컴포넌트의 역할을 명확히 하여 성능과 보안을 모두 개선할 수 있다.
Next.js는 기본 설정으로 서버 컴포넌트를 사용하도록 되어 있다. 이를 통해 개발자는 별도의 설정 없이도 자동으로 서버 사이드 렌더링(SSR)을 구현할 수 있다.
Next.js에서의 컴포넌트는 기본적으로 서버 컴포넌트이지만, 필요할 때는 클라이언트 컴포넌트로 만들 수 있다. 클라이언트 컴포넌트를 사용하려면, 파일 상단의 import 구문 위에 React의 "use client" 지시어를 추가하면 된다.
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
Next.js 14에서 "use client" 지시어는 클라이언트 컴포넌트와 서버 컴포넌트의 경계를 명확히 하는 데 사용된다. 이 지시어는 클라이언트에서 렌더링되어야 하는 컴포넌트를 정의하며, 이러한 컴포넌트는 클라이언트에서만 사용할 수 있는 API(예: useState, useEffect)를 사용할 수 있게 한다.
"use client"가 파일 상단에 정의되면, 해당 파일과 그 파일에서 불러오는 모든 모듈이 클라이언트 컴포넌트로 간주된다. 즉, 해당 컴포넌트의 자식 컴포넌트도 자동으로 클라이언트 컴포넌트로 간주된다.
클라이언트 컴포넌트 내부에서 서버 컴포넌트를 포함할 수는 있지만, 그렇게 했을 경우에는 아래와 같은 여러 문제들이 발생할 수 있다.
서버 컴포넌트는 서버에서만 실행되도록 설계되었기 때문에, 클라이언트에서 실행하려고 하면 의도하지 않은 오류가 발생할 수 있다.
또한 서버 컴포넌트 내부에서 실행되는 로직은 클라이언트에서 실행되어서는 안 되는 경우가 있기 때문에 클라이언트 컴포넌트에 서버 컴포넌트를 포함하면 이러한 민감한 정보가 노출될 위험이 있다.
서버 컴포넌트는 서버에서 실행될 때 최적화된 성능을 발휘한다. 클라이언트 컴포넌트 내부에 서버 컴포넌트를 포함하면 불필요한 서버 호출이 발생하여 성능이 저하될 수 있다.
위의 문제들을 해결하기 위해서는 다음과 같은 전략을 사용할 수 있다.
서버 컴포넌트를 최상단에 위치
SEO가 중요한 컴포넌트나 보안이 필요한 컴포넌트는 서버 컴포넌트로 두고, 트리 구조의 최상단에 배치한다.
동적이고 인터랙티브한 기능이 필요한 컴포넌트는 "use client" 사용
클라이언트 컴포넌트를 생성하여 서버 컴포넌트에 포함시킨다.
이러한 접근 방식은 상황에 맞게 유연하게 적용할 수 있으며, 다음과 같은 이점을 제공한다.
보안 유지
민감한 데이터를 서버에서 안전하게 처리할 수 있다.
성능 최적화: 초기 로딩 속도를 향상시키고, 불필요한 서버 호출을 방지할 수 있다.
SEO 개선
서버 컴포넌트에서 초기 데이터를 렌더링하여 검색 엔진 최적화(SEO)에 유리한 구조를 만들 수 있다.
해당 트리 구조를 꼭 지켜가며 개발해야 하는 것은 아니지만, 보안, 성능, SEO 측면에서 많은 이점을 얻을 수 있고, 더욱 유연하고 유지보수하기 쉬운 웹 애플리케이션을 만들 수 있다.
위와 같은 개념, 이론, 디자인 패턴을 학습해가며 Next.js 14에서의 SEO를 신경써가며 작업을 했다. SEO에는 이러한 부분들 뿐만 아니라 더 고려해야 할 부분들이 많지만, 이번 포스트는 Next.js 14에 초점을 뒀기 때문에 해당 부분만을 집중해서 다뤘다. 이외의 SEO에 대한 자세한 내용은 다른 포스트에서 다룰 예정이다.
Next.js 공식 문서
서버 컴포넌트와 클라이언트 컴포넌트
Next.js에서의 서버 및 클라이언트 컴포넌트 비교
서버 액션을 사용한 외부 API 호출