앞에서 현업&실전 프로젝트를 통해 Dynamic Open Graph 외 여러가지 방법을 통해 SEO를 개선해보았다. 이번 글에서는 layout.tsx와 metadata, generateMetadata을 활용하여 SEO개선에 대해 더 알아보자!
📌 Next.js의 레이아웃 시스템에 대해 알아보자
공통 레이아웃 요소(헤더, 네비게이션 바 등)는 웹사이트의 전반적인 사용자 경험과 구조를 개선하여 SEO에 긍정적으로 영향을 줄 수 있다. 😤
Next.js에서 layout.tsx
는 여러 페이지에 걸쳐 공유되는 UI를 정의한다. 이는 상태를 유지하고 탐색 시 재렌더링되지 않으며, 중첩될 수 있다.
상태유지: Next.js의 레이아웃은 네비게이션(페이지 간 이동)시에도 그 상태를 유지한다. 예를 들어, 사용자가 특정 인터랙티브 요소(드롭다운 메뉴 등)를 사용 중일 때 다른 페이지로 이동하면, 해당 요소의 상태(열린상태이거나 닫힌상태)가 유지된다.
재렌더링 방지: 레이아웃 컴포넌트는 페이지 이동 시 재렌더링 되지 않는다! 즉 사용자 경험이 향상되고, 애플리케이션의 성능이 개선된다.
<html>
과 <body>
태그를 포함해야 하며, 이는 Next.js가 자동으로 생성하지 않으므로 중요하다.루트 레이아웃의 역할
app/layout.tsx
파일에서 루트 레이아웃을 정의하면, 이 레이아웃이 모든 페이지에 대한 기본 HTML 구조를 설정한다. 여기에 <html>
<body>
태그를 포함시키면, 이것이 모든 페이지에 대한 기본 HTML 구조가 된다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
특정 라우트 세그먼트에 적용되며, 자식 레이아웃이나 페이지를 래핑한다. 이러한 구조는 사이트의 네비게이션과 정보 구조를 명확하게 하여, 즉 웹사이트의 구조를 명확하게 표현하여 검색 엔진 최적화에 도움이 된다!
라우트별 맞춤형 레이아웃: 중첩 레이아웃은 특정 라우트 세그먼트(ex. /dashboard
, /profile
등)에 대해 맞춤형 UI를 제공한다. 이는 각 세그먼트가 고유의 레이아웃을 가질 수 있음을 의미한다.
자식레이아웃 / 페이지 래핑: 이 레이아웃은 해당 세그먼트의 모든 페이지를 래핑하며, 공통 UI요소(ex. 사이드바, 헤더)를 제공한다.
📌 세그먼트(segment)와 Next.js
: 웹 개발과 라우팅에서 흔히 사용되는 용어로, URL의 경로를 구성하는 개별적인 부분을 의미한다.
URL 경로는 "/"
로 구분된 여러 세그먼트로 나뉜다. 각 세그먼트는 URL의 구조적 요소로서, 웹 페이지의 위치나 구조를 나타내는데 사용된다.
https://example.com/products/shoes
의 경우 두 개의 세그먼트로 구분된다. (products와 shoes)
Next.js와 같은 프레임워크에서는 이러한 URL 세그먼트를 사용하여, 어떤 페이지나 레이아웃이 특정 경로에 대응되어 표시될지 결정한다. 예를 들어, app/products/layout.tsx
는 products 세그먼트에 해당하는 모든 페이지에 대한 레이아웃을 정의할 수 있으며, app/products/shoes/page.tsx
는 products/shoes 경로에 해당하는 특정 페이지를 정의할 수 있다.
✍️ ex. 공통 UI와 맞춤형 UI를 갖는 레이아웃
루트 레이아웃 (app/layout.tsx)
export default function RootLayout({ children }) {
return (
<div>
<header>공통 헤더</header>
{children} {/* 여기에 각 페이지의 내용이 렌더링 */}
<footer>공통 푸터</footer>
</div>
);
}
마이페이지 레이아웃 (app/mypage/layout.tsx)
이 레이아웃은 /mypage 세그먼트의 모든 페이지에 적용된다.
예를 들어, 이 레이아웃에는 사이드바만 포함시킬 수 있다.
export default function MyPageLayout({ children }) {
return (
<div>
<aside>마이페이지 사이드바</aside>
{children} {/* 마이페이지 세그먼트의 각 페이지 내용이 여기에 렌더링 */}
</div>
);
}
마이페이지 내 특정 페이지 (app/mypage/somepage.tsx)
이 페이지는 마이페이지 레이아웃과 루트 레이아웃에 의해 래핑된다.
export default function SomePage() {
return <div>마이페이지의 특정 콘텐츠</div>;
}
🧐 렌더링 과정을 살펴보자
사용자가 /mypage/somepage
에 접근하면 → 루트 레이아웃이 먼저 렌더링되어 공통 헤더와 푸터를 보여줌 → 그 다음, 마이페이지 레이아웃이 렌더링되어 사이드바를 보여줌 → 마지막으로, SomePage 컴포넌트의 내용이 렌더링됨
또, app/mypage/layout.tsx
은 루트 레이아웃app/layout.tsx
에 의해 래핑되므로, 마이페이지의 모든 페이지는 루트 레이아웃에 정의된 헤더와 푸터를 포함하여 렌더링된다.
❓ 그럼 루트레이아웃에 정의한 헤더가 마이페이지에는 안보이길 원한다면? (프로젝트 때마다 정말 자주 겪었던 상황이었다.) 결국 라우트별 맞춤형 레이아웃이라는 것은 틀린 말이 아닐까? 🤨
이 부분은 결국 내가 써왔던 방법 외의 다른 방법을 알아내진 못했지만 다시 정리해보자!👇
루트 레이아웃에서 정의된 공통컴포넌트 (ex.헤더)를 특정 페이지나 세그먼트에서 숨겨보자.
1. 조건부 렌더링
: 루트 레이아웃에서 현재 라우트를 확인하고, 특정 라우트에서는 헤더를 렌더링하지 않도록 조건부 렌더링을 사용!
import { useRouter } from 'next/router';
export default function RootLayout({ children }) {
const router = useRouter();
return (
<div>
{router.pathname.startsWith('/mypage') ? null : <header>공통 헤더</header>}
{children}
<footer>공통 푸터</footer>
</div>
);
}
2. 컨텍스트 또는 글로벌 상태 사용
: 레이아웃이나 페이지 간에 공유되는 상태(예: React의 Context)를 사용하여, 특정 페이지에서 루트 레이아웃의 헤더를 숨길 수 있다.
metadata는 각각의 페이지에 대한 SEO 관련 정보를 정의하는 데 사용된다.
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Next.js',
}
export default function Page() {
return '...'
}
루트 레이아웃 또는 기본 레이아웃에서 정의된 metadata는 애플리케이션의 기본 SEO설정을 나타낸다. 여기서 설정한 값들은 특정 페이지에서 다른 metadata를 정의하지 않았을 때 적용된다.
export const metadata: Metadata = {
title: { default: 'seul의 블로그', template: 'seul의 블로그 | %s' },
description: 'seul의 개발 일지',
icons: {
icon: '/favicon.ico',
},
};
✅ %s 란?
%s
는 Next.js의 metadata 설정에서 사용되는 문자열 서식 지정자이다. 특히, title 속성에서 이를 사용하여 페이지 제목을 동적으로 설정할 수 있다.😀✨
title 객체 내의 template 속성에서%s
는 해당 페이지의 개별 metadata에서 제공된 제목으로 대체된다.// ex.) // 루트 metadata가 설정 됨 export const metadata: Metadata = { title: { default: 'seul의 블로그', template: 'seul의 블로그 | %s' }, // 다른 속성들... }; // about/page.tsx export const metadata: Metadata = { title: 'About Me', // 다른 속성들... };
이 경우,
about/page.tsx
페이지의 제목은seul의 블로그 | About Me
로 설정된다. 여기서%s
는About Me
로 대체되어, 설정한 템플릿 형식에 따라 최종 페이지 제목이 결정된다.
이렇게 템플릿을 사용하면, 페이지별로 개별적인 제목을 설정하면서도 일관된 형식을 유지할 수 있어 사이트 전반에 걸쳐 일관된 브랜딩과 SEO 친화적인 제목 구조를 갖출 수 있다.😆
각 페이지에서 별도의 metadata를 정의함으로써 해당 페이지에 특화된 SEO 정보를 제공할 수 있다. 예를들어 about/page.tsx
와 contact/page.tsx
에서 정의된 metadata는 각각의 페이지에 맞는 제목과 설명을 제공한다.
// about/page.tsx
export const metadata: Metadata = {
title: 'About Me',
description: 'seul 소개',
};
// contact/page.tsx
export const metadata: Metadata = {
title: 'Contact Me',
description: 'seul에게 메일 보내기',
};
✍️ 각 페이지에 개별적으로 metadata를 정의하면, 해당 페이지에 대한 구체적인 SEO정보(제목, 설명 등)를 제공할 수 있으며, 이것은 SEO에 매우 중요하다⭐️
사용자가 해당 페이지를 방문했을 때 브라우저 탭에 표시되는 제목이나, 검색 엔진 결과 페이지에서 보여지는 설명 등이 이 metadata에 의해 결정된다.
❓ 이렇게 간단한데 왜 이제까지 개별 설정을 했었지? 하고 알아보니.. 👇
Next.js 12 버전과 이전 버전에서는 Metadata API가 도입되기 전이므로, 페이지별로 SEO 관련 설정을 하기 위해서는 next/head 모듈을 직접 사용하여
<Head>
컴포넌트 안에 title, meta 태그 등을 명시적으로 포함시켜야 했습니다.import Head from 'next/head'; export default function MyPage() { return ( <> <Head> <title>My Page Title</title> <meta name="description" content="This is my page description." /> {/* 기타 필요한 메타 태그들 */} </Head> {/* 페이지 컨텐츠 */} </> ); }
Next.js 13에서 도입된 metadata API를 사용하면, 개별 페이지 또는 레이아웃 컴포넌트에서 SEO 관련 설정을 보다 선언적이고 간결하게 할 수 있습니다. 이 API는 각 페이지나 레이아웃에서 metadata 객체를 내보내면 자동으로 해당 정보를
<head>
태그에 적용해줍니다. < 라고한다! 😂
📌 동적 라우트 (ex. [slug])에 대한 metadata를 설정하기 위해 generateMetadata() 함수를 사용하는 방법에 대해서도 알아보자
generateMetadata() 함수를 이용하면, 페이지가 렌더링되기 전에 필요한 메타데이터 정보를 동적으로 생성하고 설정할 수 있다.
generateMetadata() 함수는 페이지의 props (여기서는 slug)에 접근하여 필요한 데이터를 얻고, 이를 바탕으로 메타데이터를 생성한다.
이 함수는 title, description과 같은 메타데이터 속성을 객체 형태로 반환해야 한다.
// app/posts/[slug]/page.tsx
...
type Props = {
params: {
slug: string;
};
};
export async function generateMetadata({ params: { slug } }: Props) {
const { title, description } = await getPostData(slug); // 1) 특정 포스트 데이터를 불러온다.
return {
title: title,
description: description,
};
}
👉 여기서 getPostData(slug) 함수를 통해 특정 게시물에 대한 데이터를 비동기적으로 불러오고, 이를 메타데이터로 설정하고 있다.
👉 nextJS에서 요구하는 해당 함수를 정의 해두면, nextJS 프레임워크가 자동으로 해당 페이지에 들어올 때마다 이 메타데이터 정보를 받아와서 페이지에 설정을 해준다
⭐️ SEO 최적화
여기서도 마찬가지로, 제공된 메타데이터는 검색 엔진 최적화(SEO)에 중요한 역할을 한다. 각 포스트의 제목과 설명이 검색 엔진 결과 페이지(SERP:Search Engine Results Page)에 어떻게 표시될지 고려하여 작성하자!
✍️12버전에서는 function getStaticProps(context){...}
을 정의하고 return된 값을 페이지 컴포넌트 props로 들어오도록해서 Head
태그에 넣어줬다.😂
정적 경로 생성
generateStaticParams 함수는 정적 사이트 생성(Static Site Generation, SSG) 과정에서 사용되며, 빌드 시 동적 라우트 세그먼트에 대한 경로를 생성한다.
동적 라우트에 대한 정적 페이지 생성
이 함수는 동적 라우트 세그먼트 ([slug])에 대한 모든 가능한 값들을 정의하며, 해당하는 각각의 페이지를 정적으로 미리 생성한다.
export async function generateStaticParams() {
const posts = await getFeaturedPosts();
return posts.map((post) => ({
params: { slug: post.path }
}));
}
✍️ generateStaticParams 함수는 getFeaturedPosts()를 호출하여 모든 주요 게시물의 slug를 가져오고 이를 통해 빌드 시간에 필요한 모든 정적 경로를 생성한다.
❓ 동적 세그먼트가 없는 정적인 라우트app/about/page.tsx
의 경우에는?
👉getStaticProps
함수만 구현해 주면 된다! (이 함수는 빌드 시에 호출되어 페이지에 필요한 데이터를 미리 가져온다😀)
Next.js 12 및 이전 버전: getStaticPaths 함수를 사용하여 정적 경로를 생성한다.
Next.js 13: generateStaticParams를 사용하여 동일한 목적을 달성할 수 있다. 이 새로운 기능은 더 세련된 방식으로 동적 라우트에 대한 정적 페이지 생성을 가능하게 한다고 한다.🧐
간단하게 SSG의 이점에 대해 다시 살펴보자!
향상된 성능
페이지가 빌드 시점에 미리 생성되므로, 사용자가 페이지를 요청할 때 빠른 로딩 시간을 제공한다. 서버에서 페이지를 실시간으로 생성할 필요가 없기 때문에, 서버 부하가 줄고 응답 시간이 단축된다.
향상된 SEO
검색 엔진은 빠르게 로드되는 페이지를 선호한다. 정적 페이지는 빠르게 로드되기 때문에 검색 엔진 최적화(SEO)에 유리하다.😤
보안 강화 동적 서버사이드 처리가 줄어들기 때문에, 보안 취약점이 감소한다.
확장성 향상
정적 파일은 CDN(Contents Delivery Network)을 통해 쉽게 배포할 수 있으며, 이는 전 세계적인 접근성과 확장성을 높여준다.
비용 절감
서버 사이드 렌더링에 비해 계산 리소스가 적게 들기 때문에, 호스팅 비용이 줄어든다.
SSG 적합한 사례
블로그나 뉴스 사이트처럼 변경 사항이 적은 컨텐츠.
마케팅 웹사이트나 문서 사이트와 같이 고정된 정보를 제공하는 페이지.
SEO 최적화가 중요한 경우.
주의사항
당연한 것이지만, 데이터가 자주 변경되는 경우에는 SSG가 적합하지 않을 수 있다. 예를 들어, 실시간 사용자 상호작용이나 데이터가 포함된 애플리케이션의 경우 서버 사이드 렌더링(SSR)이나 정적 생성과 결합된 클라이언트 사이드 렌더링(CSR)을 고려해야 할 수 있다.
또, 데이터가 변경될 때마다 사이트를 다시 빌드하고 배포해야 한다. 이는 자동화할 수 있지만, 고려해야 할 추가적인 단계가 될 수 있다.
따라서, 데이터가 빈번하게 업데이트되지 않는 경우 SSG는 웹사이트의 성능, 보안, SEO 등 여러 면에서 이점을 제공한다.
reference)
NextJS - Pages and Layouts
NextJS - generate-static-params