← cd ..
SEONext.jsGoogle Search ConsoleOpen Graph

내 사이트, 구글에서 검색되게 만들기 — Next.js SEO 세팅법

April 13, 20261 min read

사이트를 만들었는데, 구글에서 안 보인다

블로그를 열심히 만들고 포스트도 올렸는데, 구글에서 검색해보면 아무것도 안 나온다.

이게 당연한 거다. 사이트를 만든다고 구글이 알아서 찾아오지 않는다. 구글 크롤러가 사이트를 "발견"하고, "읽을 수 있고", "이해할 수 있어야" 검색 결과에 나타난다.

이걸 세팅해주는 작업이 SEO(Search Engine Optimization)다. 거창하게 들리지만 핵심은 딱 세 가지다.

  1. 크롤러가 들어올 수 있게 문 열어주기 (robots.txt)
  2. 사이트 지도 제출하기 (sitemap.xml)
  3. 각 페이지 정보 구조화하기 (메타태그, Open Graph, JSON-LD)

Next.js App Router 기준으로 이 세 가지를 직접 세팅한 과정을 공유한다.


사전 준비

  • Next.js 13+ (App Router)
  • 배포된 사이트 (도메인 필요)
  • Google Search Console 계정

Step 1. robots.txt — 크롤러에게 문 열어주기

robots.txt는 크롤러에게 "이 사이트 어디까지 봐도 돼"를 알려주는 파일이다. 없으면 크롤러가 들어와도 혼란스러울 수 있다.

Next.js App Router에서는 파일 하나로 끝난다.

app/robots.ts 생성:

import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: "https://yourdomain.com/sitemap.xml",
  };
}

빌드하면 자동으로 /robots.txt가 생성된다. yourdomain.com은 실제 도메인으로 바꿔주면 된다.


Step 2. sitemap.xml — 구글에 사이트 지도 제출

sitemap은 "내 사이트에는 이런 페이지들이 있어요"를 구글에 알려주는 지도다. 없으면 구글이 직접 크롤링해서 찾아야 하는데, 그러면 누락되는 페이지가 생긴다.

app/sitemap.ts 생성:

import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";

const BASE_URL = "https://yourdomain.com";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();

  const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${BASE_URL}/posts/${post.slug}`,
    lastModified: post.date ? new Date(post.date) : new Date(),
    changeFrequency: "monthly",
    priority: 0.8,
  }));

  const staticEntries: MetadataRoute.Sitemap = [
    { url: BASE_URL,              changeFrequency: "daily",   priority: 1.0 },
    { url: `${BASE_URL}/posts`,   changeFrequency: "daily",   priority: 0.9 },
    { url: `${BASE_URL}/about`,   changeFrequency: "monthly", priority: 0.5 },
  ];

  return [...staticEntries, ...postEntries];
}

빌드 후 /sitemap.xml에서 확인할 수 있다.

다국어 사이트라면? next-intl 같은 i18n 라이브러리를 쓰고 있다면, /ko/posts/slug/en/posts/slug를 모두 포함해야 한다. 그리고 fallback으로 보여주는 페이지(번역 없이 다른 언어 내용을 보여주는 경우)는 sitemap에서 제외하는 게 좋다. 중복 콘텐츠로 인식될 수 있기 때문이다.


Step 3. Open Graph — 공유했을 때 예쁘게 나오게

카카오톡, 슬랙, 트위터에 링크를 붙여넣으면 제목과 설명이 미리보기로 뜨는 걸 본 적 있을 거다. 그게 Open Graph 태그 덕분이다. 없으면 링크 텍스트만 달랑 나온다.

app/[locale]/layout.tsx (사이트 전역):

export async function generateMetadata(): Promise<Metadata> {
  return {
    title: {
      default: "내 블로그 이름",
      template: `%s | 내 블로그 이름`,  // 포스트 페이지에서 "포스트제목 | 블로그이름" 형식
    },
    description: "블로그 설명",
    metadataBase: new URL("https://yourdomain.com"),
    openGraph: {
      type: "website",
      siteName: "내 블로그 이름",
      title: "내 블로그 이름",
      description: "블로그 설명",
    },
    twitter: {
      card: "summary_large_image",
    },
  };
}

app/[locale]/posts/[slug]/page.tsx (포스트 개별 페이지):

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug, locale } = await params;
  const post = await getPost(slug, locale);
  if (!post) return {};

  return {
    title: post.title,
    description: post.description,
    alternates: {
      canonical: `https://yourdomain.com/${locale}/posts/${slug}`,
    },
    openGraph: {
      type: "article",           // 블로그 글임을 명시
      title: post.title,
      description: post.description,
      publishedTime: post.date ? new Date(post.date).toISOString() : undefined,
      tags: post.tags,
    },
  };
}

canonical은 "이 페이지의 정식 URL은 여기야"라고 알려주는 것이다. 다국어 사이트에서 같은 내용이 여러 URL에 있을 때 중복 콘텐츠 패널티를 막아준다.


Step 4. JSON-LD — 구글이 블로그 글임을 확실히 인식

JSON-LD는 페이지에 숨겨진 구조화 데이터다. 구글이 이걸 읽고 "아, 이건 블로그 포스트구나"라고 정확히 이해한다. 잘 되면 검색 결과에 리치카드(작성일, 태그 등)가 표시되기도 한다.

포스트 페이지 컴포넌트 안에 추가:

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  description: post.description,
  datePublished: post.date ? new Date(post.date).toISOString() : undefined,
  url: `https://yourdomain.com/posts/${slug}`,
  author: {
    "@type": "Person",
    name: "블로그 작성자 이름",
    url: "https://yourdomain.com",
  },
  keywords: post.tags?.join(", "),
};

return (
  <article>
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
    {/* 나머지 포스트 내용 */}
  </article>
);

Step 5. Google Search Console 등록 및 sitemap 제출

여기까지 했으면 이제 구글한테 직접 알려줘야 한다.

1. Search Console 접속 및 속성 추가

search.google.com/search-console → 속성 추가 → URL 접두어 → 도메인 입력

2. 소유권 확인

HTML 태그 방법 선택 → 아래 같은 코드 발급:

<meta name="google-site-verification" content="여기에값이들어옴" />

Next.js에서는 generateMetadata에 한 줄 추가:

verification: {
  google: "여기에값이들어옴",
},

배포 후 Search Console에서 확인 클릭.

3. sitemap 제출

좌측 메뉴 → Sitemapssitemap.xml 입력 → 제출


트러블슈팅

sitemap.xml에 접속했을 때 빈 화면이 나온다

  • 빌드가 완료됐는지 확인
  • getAllPosts() 같은 데이터 fetch 함수에서 에러가 나는지 확인
  • 프로덕션에서만 동작하는 환경변수가 있다면 Vercel 환경변수 세팅 확인

Search Console에서 "URL을 가져올 수 없음" 오류

  • robots.txt에서 해당 URL을 차단하고 있지 않은지 확인
  • 사이트가 실제로 배포된 상태인지 확인
  • Vercel 배포 직후라면 1~2분 대기 후 재시도

메타태그가 적용됐는지 확인하는 법

브라우저에서 페이지 소스 보기(Cmd+U)로 <meta property="og:title" 검색하거나, opengraph.xyz에서 URL 입력하면 미리보기로 확인 가능하다.


정리 — 핵심 흐름 한눈에

1. app/robots.ts      → 크롤러 허용 + sitemap 위치 안내
2. app/sitemap.ts     → 모든 페이지 URL 목록 생성
3. layout.tsx         → 사이트 전역 OG/Twitter 메타태그
4. posts/[slug]/page  → 포스트별 canonical + OG article + JSON-LD
5. Search Console     → 소유권 인증 → sitemap 제출

한 번 세팅해두면 이후 포스트를 올릴 때마다 sitemap이 자동으로 업데이트된다. 검색 노출은 제출 후 1~2주 정도 걸리는 게 일반적이다. 기다리는 동안 포스트를 꾸준히 올리는 게 가장 좋은 SEO 전략이다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.

내 사이트, 구글에서 검색되게 만들기 — Next.js SEO 세팅법 | backtodev