SEO는 코드로 끝나지 않았다: 가비아 도메인과 Vercel, Google Search Console로 myblog 연결하기

작성일: 2026년 8월 12일 PM 01:22
조회수: 1

thumbnail

가비아 DNS 관리 화면에서 myblog 서브도메인과 여러 서비스의 레코드를 확인하는 모습

앞선 글에서 Next.js 프로젝트 안에 SEO 기능을 구현했습니다. title, description, canonical URL, JSON-LD, sitemap, robots.txt, RSS, OG 이미지까지 만들었습니다.

그때는 이렇게 생각하기 쉽습니다.

이제 검색엔진이 내 블로그를 알아서 찾아오겠지?

그런데 실제로는 한 단계가 더 필요했습니다. 코드 안의 사이트 주소는 아직 Vercel이 자동으로 만들어 준 주소를 바라보고 있었고, 새로 구매한 도메인과 Google Search Console에는 myblog를 연결하지 않은 상태였습니다.

결정적으로 저는 운영 환경변수의 사이트 주소를 바꾼 뒤 재배포를 하지 않았습니다. 환경변수 파일의 글자만 바뀌었을 뿐, 실제로 배포된 웹사이트가 새 값을 사용하도록 만드는 과정이 빠져 있었습니다.

이 글은 그 실수를 포함해, 완성된 Next.js 프로젝트를 실제 도메인에 연결하고 검색엔진에 알려주는 과정을 myblog 사례로 정리한 글입니다. SEO 기능을 코드로 구현하는 방법은 앞선 글에서 다루었고, 이번 글에서는 그 코드가 운영 사이트에서 제대로 작동하도록 연결하는 데 집중합니다.

이 글의 목표: SEO 구현과 운영 연결을 구분하기

SEO에는 두 종류의 일이 있습니다.

첫 번째는 코드 안에서 검색엔진과 소셜 서비스가 읽을 정보를 준비하는 일입니다.

  • canonical URL을 올바르게 출력하기
  • 게시글마다 title과 description 만들기
  • JSON-LD로 게시글 의미 설명하기
  • sitemap.xml과 robots.txt 제공하기
  • 공유할 때 사용할 OG 이미지 만들기

두 번째는 그 코드가 실제 운영 주소에서 동작하도록 연결하는 일입니다.

  • 가비아에서 도메인 DNS 설정하기
  • Vercel 프로젝트에 서브도메인 연결하기
  • NEXT_PUBLIC_SITE_URL을 운영 주소로 바꾸기
  • 변경된 환경변수로 다시 배포하기
  • Google Search Console에서 사이트를 인증하고 sitemap 제출하기
  • 예전 Vercel 주소에서 새 도메인으로 이동시키기

첫 번째만 끝내면 “SEO 관련 코드는 있다”라고 말할 수 있습니다. 두 번째까지 끝내야 “사람과 검색엔진이 실제 운영 사이트를 찾을 수 있다”에 가까워집니다.

단, 여기서도 검색 결과 노출이나 검색 순위를 보장할 수는 없습니다. 우리가 할 수 있는 일은 검색엔진이 올바른 주소를 방문하고, 내용을 읽고, 사이트 구조를 발견할 수 있도록 정확한 신호를 제공하는 것입니다.

전체 흐름을 먼저 보기

myblog에서 이번에 연결한 흐름은 다음과 같습니다.

mermaid
다이어그램 정의(mermaid):
flowchart LR
    A[가비아에서 구매한 kyy.co.kr] --> B[myblog.kyy.co.kr DNS]
    B --> C[Vercel myblog 프로젝트]
    C --> D[NEXT_PUBLIC_SITE_URL]
    D --> E[canonical · OG · JSON-LD]
    D --> F[sitemap · robots · RSS]
    F --> G[Google Search Console]
    H[기존 Vercel 주소] --> I[새 도메인으로 redirect]

이 그림에서 중요한 점은 NEXT_PUBLIC_SITE_URL입니다. 사이트 주소를 여러 파일에 직접 적어 두는 대신 한 곳에서 관리하면, 운영 주소가 바뀌었을 때 SEO 관련 출력도 같은 기준을 사용하게 만들 수 있습니다.

1. 먼저 도메인 구조를 정합니다

도메인이란 무엇인가요?

도메인은 웹사이트를 찾아가기 위한 사람이 읽을 수 있는 주소입니다.

Vercel은 프로젝트를 만들면 다음처럼 기본 주소를 줍니다.

text
myblog-navy-kappa.vercel.app

이 주소로도 사이트는 열립니다. 하지만 개인 블로그를 오래 운영하려면 내가 소유한 도메인을 사용하는 편이 좋습니다.

이번에 가비아에서 kyy.co.kr을 구매했습니다. 여기서 kyy.co.kr을 루트 도메인이라고 부르고, 그 앞에 이름을 붙인 주소를 서브도메인이라고 부릅니다.

text
루트 도메인: kyy.co.kr
서브도메인: myblog.kyy.co.kr

이번에는 루트 도메인 자체를 하나의 사이트로 사용하지 않고, 여러 Vercel 프로젝트를 서로 다른 서브도메인에 연결했습니다.

text
myblog.kyy.co.kr       → myblog Vercel 프로젝트
seongryung.kyy.co.kr   → seongryung Vercel 프로젝트
potfolio.kyy.co.kr     → potfolio Vercel 프로젝트

www.kyy.co.kr을 사용하지 않아도 문제없습니다. www는 필수 기능이 아니라 많은 사이트가 관습적으로 사용하는 하나의 서브도메인일 뿐입니다. 내 서비스의 공식 주소를 myblog.kyy.co.kr로 정했다면 그 주소를 모든 SEO 설정의 기준으로 삼으면 됩니다.

왜 이 구조가 유용한가요?

도메인 하나를 구매해 여러 사이트에 나누어 사용할 수 있습니다. 각각의 서비스는 별도 Vercel 프로젝트로 관리하면서도 방문자에게는 같은 개인 브랜드 아래에 있는 서비스처럼 보입니다.

다만 서브도메인은 서로 다른 호스트입니다. myblog.kyy.co.krseongryung.kyy.co.kr은 주소가 다르므로, 각 Vercel 프로젝트에 별도로 연결하고 각 사이트의 공식 URL도 따로 설정해야 합니다.

2. 가비아 DNS에서 서브도메인을 Vercel로 연결합니다

DNS는 어떤 역할을 하나요?

DNS는 도메인 주소와 실제 서비스를 연결하는 주소록입니다.

사람이 브라우저에 myblog.kyy.co.kr을 입력하면 브라우저는 DNS에 “이 주소를 어느 서버로 찾아가야 하나요?”라고 묻습니다. DNS에 Vercel 연결 정보를 넣어 두면 요청이 올바른 Vercel 프로젝트로 이동합니다.

myblog에 필요한 레코드

Vercel 프로젝트의 Domains 화면에서 myblog.kyy.co.kr을 추가하면 연결에 필요한 DNS 레코드를 보여줍니다. 정확한 값은 시점과 프로젝트에 따라 달라질 수 있으므로, 아래 값을 무조건 복사하기보다 Vercel 화면에서 안내한 값을 사용해야 합니다.

일반적인 형태는 다음과 같습니다.

레코드호스트값의 역할
CNAMEmyblogmyblog.kyy.co.kr을 Vercel이 안내한 대상에 연결
TXTmyblog 또는 안내된 호스트도메인 소유권 또는 서비스 연결 확인
A@루트 도메인 kyy.co.kr에 사용하는 별도 설정

스크린샷의 myblog, seongryung, potfolio CNAME은 각각 다른 서브도메인을 각자의 Vercel 프로젝트에 연결하기 위한 기록입니다. TXT 기록은 Google Search Console이나 Vercel이 “이 도메인을 실제로 관리하는 사람이 누구인지” 확인할 때 사용됩니다.

DNS 호스트 입력란에서 서브도메인 이름을 어떻게 적을지는 DNS 제공업체마다 표현이 조금 다를 수 있습니다. 가비아에서 myblog만 요구하는지 myblog.kyy.co.kr 전체를 요구하는지는 입력 화면의 안내를 따르세요. 같은 레코드를 여러 번 만들면 충돌할 수 있으므로 기존 기록도 함께 확인해야 합니다.

DNS 변경 후 바로 안 열릴 수 있습니다

DNS는 바뀌자마자 전 세계 모든 네트워크에 동시에 반영되지 않을 수 있습니다. 브라우저 캐시, 통신사 DNS 캐시, TTL 설정 때문에 잠시 이전 결과가 보일 수 있습니다.

따라서 다음 순서로 확인합니다.

  1. 가비아 DNS에 Vercel이 안내한 CNAME을 저장합니다.
  2. Vercel Domains 화면에서 도메인 상태가 Valid Configuration인지 확인합니다.
  3. https://myblog.kyy.co.kr을 직접 열어 사이트가 보이는지 확인합니다.
  4. HTTPS 인증서가 정상으로 발급되었는지 브라우저 주소창에서 확인합니다.

3. Vercel 프로젝트에 custom domain을 추가합니다

Vercel의 기본 주소와 custom domain

Vercel의 기본 주소는 프로젝트를 빠르게 확인하기에 좋습니다. 하지만 운영 주소는 내가 정한 custom domain으로 통일하는 것이 좋습니다.

Vercel 프로젝트의 Domains 메뉴에서 다음 주소를 추가합니다.

text
myblog.kyy.co.kr

Vercel이 제시한 DNS 값을 가비아에 입력한 뒤, Vercel 화면에서 검증이 끝나면 custom domain이 프로젝트에 연결됩니다.

Vercel에서 custom domain과 기존 vercel.app 주소의 redirect를 설정한 화면

이 화면에서 위쪽의 myblog.kyy.co.kr은 실제 운영 주소이고, 아래쪽의 myblog-navy-kappa.vercel.app은 Vercel이 기본으로 제공한 주소입니다.

기존 Vercel 주소는 어떻게 할까요?

기존 주소를 그냥 남겨 두면 같은 게시글이 두 주소에서 열릴 수 있습니다.

text
https://myblog-navy-kappa.vercel.app/posts/157
https://myblog.kyy.co.kr/posts/157

이런 상황에서는 방문자를 새 공식 주소로 이동시키는 redirect를 설정할 수 있습니다. 이때 경로도 유지되어야 합니다.

text
/posts/157
→ https://myblog.kyy.co.kr/posts/157

스크린샷의 현재 설정은 307 Temporary Redirect입니다. 307은 “일시적으로 다른 곳에 보내라”는 의미입니다. DNS와 custom domain, canonical이 모두 정상인지 테스트하는 단계에서는 안전한 선택일 수 있습니다.

운영 주소가 확정되고 이전 주소를 영구적으로 옮길 계획이라면 308 Permanent Redirect를 검토할 수 있습니다. 영구 이동은 브라우저와 검색엔진이 새 주소를 공식 주소로 이해하는 데 더 적합하지만, 되돌릴 가능성이 있다면 먼저 307로 확인한 뒤 바꾸는 편이 안전합니다.

redirect가 모든 경로에서 동작하는지 확인하세요. 홈만 새 주소로 보내고 /posts/157 같은 상세 경로가 끊기면 방문자와 검색엔진 모두 불편을 겪습니다.

4. 사이트 주소를 코드에 하드코딩하지 않습니다

이번 작업에서 가장 중요한 개발 원칙입니다.

나쁜 예: 여러 파일에 주소를 직접 적기

처음에는 아래처럼 작성하기 쉽습니다.

ts
const siteUrl = 'https://myblog-navy-kappa.vercel.app';

그리고 다른 파일에도 같은 주소를 복사합니다.

ts
const canonical = 'https://myblog-navy-kappa.vercel.app/posts/157';
const sitemapUrl = 'https://myblog-navy-kappa.vercel.app/sitemap.xml';
const feedUrl = 'https://myblog-navy-kappa.vercel.app/feed.xml';

이 방식은 주소를 바꿀 때 실수하기 쉽습니다. canonical만 바꾸고 sitemap을 빼먹거나, OG 이미지 URL만 이전 주소를 가리키는 일이 생길 수 있습니다.

좋은 예: 환경변수와 공통 함수 사용하기

myblog는 운영 사이트 주소를 환경변수로 관리합니다.

env
NEXT_PUBLIC_SITE_URL=https://myblog.kyy.co.kr

그리고 src/lib/site-config.ts에서 값을 검사하고 사용합니다.

ts
export function getSiteUrl(): string {
  return parseSiteUrl(process.env.NEXT_PUBLIC_SITE_URL);
}

export function toSiteUrl(path = '/'): string {
  const relativePath = path.replace(/^\/+/, '');
  return new URL(relativePath, `${getSiteUrl()}/`).toString();
}

이 함수는 단순히 문자열을 꺼내는 역할만 하지 않습니다.

  • 환경변수가 없으면 오류를 냅니다.
  • 상대 경로가 아니라 절대 URL인지 확인합니다.
  • 운영 환경에서는 HTTPS만 허용합니다.
  • 주소 끝의 slash를 정리합니다.

이렇게 하면 주소를 바꿀 때 모든 SEO 파일을 찾아다니며 문자열을 교체할 필요가 줄어듭니다.

환경변수만 바꾸면 코드가 자동으로 바뀌나요?

정확히 표현하면 코드가 스스로 수정되는 것은 아닙니다. 코드 파일이 NEXT_PUBLIC_SITE_URL을 읽도록 설계되어 있고, 새 환경변수로 다시 실행하거나 다시 배포하면 생성되는 결과가 바뀌는 것입니다.

예를 들어 다음 파일들이 같은 주소 정책을 사용합니다.

출력연결된 구현
canonical게시글 generateMetadata()toSiteUrl()
Open Graph URL게시글 metadata와 getPostSeoFields()
JSON-LD URL게시글 페이지의 BlogPosting, BreadcrumbList
sitemap URLsrc/app/sitemap.ts
robots sitemap 주소src/app/robots.ts
RSS 링크src/app/feed.xml/route.ts

따라서 환경변수만 바꿔도 자연스럽게 전체 출력이 새 주소를 바라보게 만들 수 있습니다. 단, 변경된 환경변수를 사용하도록 배포를 다시 해야 합니다. 이번에 제가 놓친 부분이 바로 이것이었습니다.

5. 환경변수를 바꾼 뒤 반드시 재배포합니다

제가 실제로 놓친 과정

도메인을 Vercel에 연결한 뒤 운영 환경변수의 값을 다음처럼 바꾸었습니다.

env
NEXT_PUBLIC_SITE_URL=https://myblog.kyy.co.kr

하지만 재배포를 하지 않았습니다. 그 결과 Vercel에서 이미 실행 중인 배포는 계속 예전 값을 사용했고, sitemap·robots·canonical 같은 출력도 이전 Vercel 주소를 기준으로 남아 있을 수 있었습니다.

환경변수는 메모장에 적힌 설정이 아니라 배포된 애플리케이션이 읽는 입력값입니다. Vercel의 Production 환경변수를 바꿨다면 새 배포를 만들어야 실제 Production 응답에 반영됩니다.

올바른 순서

  1. Vercel 프로젝트의 Settings → Environment Variables로 이동합니다.
  2. Production 환경의 NEXT_PUBLIC_SITE_URL을 확인합니다.
  3. 값을 https://myblog.kyy.co.kr로 변경합니다.
  4. 저장합니다.
  5. 새 Production 배포를 실행합니다.
  6. 배포가 완료된 뒤 실제 도메인의 응답을 확인합니다.

Preview 환경과 Production 환경이 서로 다른 값을 가지고 있을 수 있다는 점도 주의해야 합니다.

text
Development → http://localhost:3000
Preview     → 테스트용 주소 또는 Preview 도메인
Production  → https://myblog.kyy.co.kr

운영 SEO를 확인하는데 Preview 환경변수만 바꾸면 실제 검색엔진이 방문하는 사이트에는 아무 변화가 없습니다. 어느 환경의 값을 수정했는지와 어느 환경으로 배포했는지를 같이 확인해야 합니다.

6. 배포된 사이트에서 SEO 출력이 바뀌었는지 확인합니다

재배포가 끝나면 “Vercel 배포 성공”만 보고 끝내지 않습니다. 브라우저와 터미널에서 실제 운영 주소가 어떤 응답을 주는지 확인합니다.

robots.txt

bash
curl -s https://myblog.kyy.co.kr/robots.txt

다음처럼 새 도메인 기준의 sitemap 주소가 나와야 합니다.

text
Sitemap: https://myblog.kyy.co.kr/sitemap.xml

sitemap.xml

bash
curl -s https://myblog.kyy.co.kr/sitemap.xml

<loc> 안의 URL이 myblog.kyy.co.kr로 시작하는지 확인합니다. 검색하지 않을 /search가 sitemap에 들어가 있지 않은지도 확인합니다.

게시글 HTML

bash
curl -s https://myblog.kyy.co.kr/posts/157 | rg 'canonical|og:image|application/ld\+json'

다음 항목을 확인합니다.

  • canonical이 https://myblog.kyy.co.kr/posts/157인지
  • og:image가 원본 썸네지 또는 생성 OG 카드 주소인지
  • BlogPosting JSON-LD가 있는지
  • BreadcrumbList JSON-LD가 있는지

개발자 도구의 Elements 탭에서 직접 확인해도 됩니다. 중요한 것은 metadata 객체의 코드가 아니라 브라우저가 실제로 받은 HTML입니다.

7. Google Search Console에 사이트를 등록합니다

Search Console은 무엇인가요?

Google Search Console은 Google 검색에서 내 사이트가 어떻게 발견되고 처리되는지 확인하는 도구입니다.

검색 순위를 올려주는 버튼은 아닙니다. 대신 다음 정보를 확인할 수 있습니다.

  • Google이 사이트를 발견했는지
  • sitemap을 읽었는지
  • 어떤 페이지를 발견했는지
  • 색인에 문제가 있는지
  • 검색 결과에서 어떤 검색어로 노출되었는지

속성 추가와 소유권 확인

Google Search Console에서 myblog의 공식 주소를 등록합니다.

text
https://myblog.kyy.co.kr

Google이 안내하는 소유권 확인 방법 중 DNS TXT 방식을 선택하면 가비아 DNS에 TXT 레코드를 추가합니다. TXT 값은 사이트마다 다르므로 Google 화면에서 발급한 값을 그대로 사용해야 합니다.

도메인 속성으로 kyy.co.kr 전체를 관리할 수도 있고, URL 접두어 속성으로 https://myblog.kyy.co.kr만 관리할 수도 있습니다. 여러 서브도메인을 각각 확인하고 싶다면 Search Console 화면에서 실제로 관리하려는 주소가 어떤 속성으로 등록되었는지 확인하세요.

이번 myblog 설정에서는 가비아 DNS에 myblog용 TXT 레코드를 등록하고, Search Console에서 myblog 사이트의 소유권을 확인했습니다.

sitemap을 제출합니다

Search Console의 Sitemaps 메뉴에 다음 주소를 제출합니다.

text
https://myblog.kyy.co.kr/sitemap.xml

Google Search Console에서 myblog sitemap이 성공적으로 처리된 화면

이번 설정에서는 sitemap 상태가 성공으로 표시되었고, Google Search Console에서 28개의 페이지를 발견한 것으로 확인했습니다.

하지만 “발견된 페이지 28개”와 “검색 결과에 색인된 페이지 28개”는 같은 말이 아닙니다.

  • sitemap 제출: Google에게 사이트의 페이지 목록을 알려주는 일
  • 발견: Google이 sitemap에서 주소를 읽은 일
  • 크롤링: Googlebot이 해당 주소에 방문한 일
  • 색인: 검색 결과 후보 목록에 페이지를 저장한 일
  • 노출: 실제 검색어 결과에 페이지가 표시된 일

각 단계 사이에는 시간이 걸릴 수 있고, Google이 모든 URL을 색인한다고 보장하지도 않습니다. Search Console의 색인 생성 보고서와 URL 검사 도구에서 개별 페이지 상태를 따로 확인해야 합니다.

8. myblog 코드에서 sitemap·robots·RSS가 새 도메인을 사용하는 이유

환경변수와 공통 URL 함수가 연결되어 있으면 운영 주소를 바꿔도 파일마다 직접 수정할 필요가 없습니다.

sitemap

src/app/sitemap.tsgetSiteUrl()을 사용해 홈, 글 목록, 게시글 상세 URL을 만듭니다.

ts
const siteUrl = getSiteUrl();

entries.push({
  url: `${siteUrl}/posts/${post.id}`,
});

검색용이 아닌 /search는 sitemap에 넣지 않습니다. 검색 페이지는 noindex, follow이기 때문입니다.

현재 sitemap은 게시글을 최대 10,000개까지 수집하도록 안전장치를 두고 있습니다. 게시글이 그보다 많아지면 sitemap 분할이 필요합니다. 데이터베이스 오류가 발생하면 홈과 글 목록 같은 정적 엔트리만 반환할 수 있다는 점도 운영상 한계입니다.

robots.txt

src/app/robots.ts는 검색봇에게 사이트의 기본 접근 규칙과 sitemap 위치를 알려줍니다.

ts
return {
  rules: [{ userAgent: '*', allow: '/', disallow: ['/admin/'] }],
  sitemap: `${siteUrl}/sitemap.xml`,
  host: siteUrl,
};

robots.txt는 보안 장치가 아닙니다. 관리자 데이터나 비공개 데이터는 서버 인증과 권한 검사로 보호해야 합니다.

RSS

src/app/feed.xml/route.ts는 최신 게시글을 RSS 2.0 형식으로 제공합니다. 제목, 본문에서 뽑은 설명, 게시글 URL, 작성일, 해시태그를 XML로 작성합니다.

RSS의 특수문자를 XML 규칙에 맞게 escape하고, 최신 글 조회 결과를 1시간 동안 재사용하도록 설정했습니다. RSS는 검색엔진 전용 파일은 아니지만, 독자나 피드 리더가 새 글을 발견하는 또 하나의 통로입니다.

9. OG 이미지와 썸네지를 확인합니다

SEO 구현을 마쳤더라도 링크를 카카오톡이나 메신저에 붙여 넣었을 때 어떤 이미지가 나오는지 확인해야 합니다.

myblog 게시글의 이미지 선택 우선순위는 다음과 같습니다.

  1. 게시글에 안전한 HTTPS thumbnail_url이 있으면 원본 썸네지를 사용합니다.
  2. 썸네지가 없거나 유효하지 않으면 게시글 opengraph-image 생성 카드를 사용합니다.

이때 metadata에 fallback 이미지를 임의로 넣지 않고, 썸네지를 사용하는 경우에만 openGraph.imagestwitter.images를 넣습니다. fallback일 때 images: undefined도 넣지 않습니다.

Next.js의 파일 기반 opengraph-image.tsx가 자동으로 선택될 수 있게 하려면 이미지 속성 자체가 없는 상태가 필요하기 때문입니다.

ts
const socialImageMetadata = getSocialImageMetadata(post.thumbnail_url);

return {
  openGraph: {
    // ...
    ...socialImageMetadata.openGraph,
  },
  twitter: {
    card: 'summary_large_image',
    // ...
    ...socialImageMetadata.twitter,
  },
};

유효성 검사는 우선 안전한 절대 HTTPS URL인지 확인하는 수준입니다. 매 요청마다 원격 이미지에 HEAD 요청을 보내지는 않습니다. metadata 생성이 느려지거나 외부 서비스 상태에 영향을 받지 않게 하기 위해서입니다.

생성 OG 카드와 한글 폰트

썸네지가 없는 게시글은 src/app/posts/[id]/opengraph-image.tsx가 게시글 제목과 태그를 사용해 브랜드 카드를 생성합니다.

next/og 이미지 렌더러는 실행 환경에 한글 폰트가 있다고 가정할 수 없습니다. 그래서 myblog는 다음 폰트를 저장소에 포함했습니다.

text
src/assets/fonts/NotoSansKR-Regular.ttf
src/assets/fonts/NotoSansKR-Bold.ttf

src/lib/og-font.ts는 이 파일을 읽고 모듈 단위 Promise 캐시에 저장합니다. 같은 프로세스에서 반복 요청이 들어와도 매번 파일을 다시 읽는 비용을 줄이는 방식입니다.

외부 Google Fonts를 OG 이미지 생성 시점에 fetch하지 않으므로, 해당 route는 네트워크가 차단된 환경에서도 한글 폰트를 사용할 수 있습니다.

폰트 파일의 라이선스와 출처는 src/assets/fonts/README.md에 기록했습니다. 저장소에 폰트를 넣을 때는 “인터넷에 검색되었다”만으로 충분하지 않고, 실제 배포와 재배포가 가능한 라이선스인지 확인해야 합니다.

다만 이 사실을 “전체 프로젝트가 완전히 오프라인 빌드된다”라고 확대하면 안 됩니다. 전역 layout에서는 next/font/google도 사용하고 있으므로, OG route의 로컬 폰트와 전체 Next.js 빌드의 네트워크 의존성은 서로 다른 범위의 이야기입니다.

10. 직접 따라 하는 검증 순서

다음 순서를 그대로 실행하면 도메인 연결부터 SEO 출력까지 확인할 수 있습니다.

1) 도메인과 redirect 확인

bash
curl -I https://myblog.kyy.co.kr
curl -I https://myblog-navy-kappa.vercel.app/posts/157

새 도메인은 200 응답인지, 예전 주소는 새 도메인으로 이동하는지 확인합니다. redirect 상태 코드는 현재 Vercel 설정에 따라 307일 수 있습니다.

2) robots와 sitemap 확인

bash
curl -s https://myblog.kyy.co.kr/robots.txt
curl -s https://myblog.kyy.co.kr/sitemap.xml

다음 조건을 확인합니다.

  • sitemap URL이 https://myblog.kyy.co.kr/sitemap.xml인지
  • sitemap의 <loc>에 예전 Vercel 주소가 없는지
  • sitemap에 /search가 없는지
  • XML이 중간에 깨지지 않는지

3) RSS와 manifest 확인

bash
curl -I https://myblog.kyy.co.kr/feed.xml
curl -s https://myblog.kyy.co.kr/feed.xml
curl -I https://myblog.kyy.co.kr/manifest.webmanifest

RSS가 200으로 응답하고 XML 특수문자가 깨지지 않는지 확인합니다. manifest 안의 아이콘 경로도 실제로 열리는지 확인합니다.

4) 게시글 HTML 확인

bash
curl -s https://myblog.kyy.co.kr/posts/157 \
  | rg 'canonical|og:image|application/ld\\+json|BreadcrumbList|BlogPosting'

썸네지가 있는 게시글은 원본 HTTPS URL이 og:image에 들어가야 합니다. 썸네지가 없는 게시글은 /posts/<id>/opengraph-image 주소가 사용되어야 합니다.

5) 생성 OG 이미지 확인

bash
curl -I https://myblog.kyy.co.kr/opengraph-image
curl -I https://myblog.kyy.co.kr/posts/157/opengraph-image

응답이 200이고 image/png로 반환되는지 확인합니다. 한글 제목이 들어간 게시글의 경우 실제 이미지를 열어 글자가 네모나 깨진 글꼴로 보이지 않는지도 확인합니다.

6) 프로젝트 자동 검사

코드 변경까지 포함된 작업이라면 프로젝트 루트에서 다음 검사를 실행합니다.

bash
npm run test
npx tsc --noEmit
npm run lint
npm run build
git diff --check

이번 SEO 작업에서는 Markdown description 추출, 단어 수 계산, 사이트 URL 검증, OG 이미지 선택을 Vitest로 확인했습니다. 단위 테스트는 “코드가 정한 규칙대로 동작하는가”를 검증하고, 실제 도메인 연결과 Google 색인 상태까지 증명하지는 않습니다.

11. 이번 myblog에서 완료된 것

이번 작업에서 코드와 운영 설정을 연결한 결과는 다음과 같습니다.

  • NEXT_PUBLIC_SITE_URL을 공식 사이트 주소의 단일 기준으로 사용했습니다.
  • 운영 환경변수를 https://myblog.kyy.co.kr로 변경하고 재배포했습니다.
  • canonical, OG, JSON-LD, sitemap, robots, RSS가 같은 사이트 URL 정책을 사용합니다.
  • 가비아 DNS에서 myblog CNAME과 소유권 확인 TXT 레코드를 설정했습니다.
  • Vercel 프로젝트에 myblog.kyy.co.kr custom domain을 연결했습니다.
  • Google Search Console에서 myblog 속성을 확인했습니다.
  • https://myblog.kyy.co.kr/sitemap.xml을 Search Console에 제출했습니다.
  • Search Console에서 sitemap 처리 성공과 발견된 페이지 수를 확인했습니다.
  • 기존 myblog-navy-kappa.vercel.app 주소를 새 도메인으로 redirect하도록 설정했습니다.
  • myblog의 여러 사이트 주소를 한 문자열로 하드코딩하지 않고 환경변수와 공통 helper를 통해 관리합니다.

12. 아직 보장하지 않는 것과 주의할 점

완료 목록이 있다고 해서 모든 문제가 해결된 것은 아닙니다.

검색 순위와 색인을 보장하지 않습니다

SEO 코드를 잘 작성하고 sitemap을 제출해도 Google이 모든 페이지를 색인하거나 특정 검색어에서 높은 순위를 보장하지는 않습니다. 콘텐츠 품질, 사이트 신뢰도, 중복 여부, 크롤링 일정, 검색어 경쟁 등 여러 요소가 함께 영향을 줍니다.

이번 범위에서는 Google Search Console에만 등록했습니다. 한국 사용자 대상 사이트라면 Naver Search Advisor 등록도 다음 작업 후보가 될 수 있지만, 등록하지 않았다고 사이트가 작동하지 않는 것은 아닙니다.

www는 사용하지 않았습니다

이번 공식 주소는 myblog.kyy.co.kr입니다. 루트 도메인이나 www 주소를 사용하지 않으므로 해당 주소를 canonical이나 sitemap에 넣지 않았습니다. 나중에 공식 주소를 추가하거나 바꾸면 redirect와 환경변수, Search Console 속성을 함께 검토해야 합니다.

redirect는 상태 코드를 확인해야 합니다

현재 확인한 Vercel 화면에는 기존 주소가 307 Temporary Redirect로 표시되어 있습니다. 장기적인 주소 이전이 확정되면 308 Permanent Redirect를 검토할 수 있지만, 변경 전에 모든 경로와 query가 정상적으로 이동하는지 테스트해야 합니다.

/search/posts query URL의 정책이 다릅니다

/searchnoindex, follow이며 sitemap에도 넣지 않습니다. 하지만 /posts?sort=..., /posts?tag=... 같은 목록 query URL은 아직 별도로 색인을 닫는 정책이 명시되어 있지 않습니다. canonical이 /posts를 가리키더라도 query URL의 노출 문제가 자동으로 모두 사라지는 것은 아닙니다.

OG route 폰트와 전체 빌드의 범위는 다릅니다

OG 이미지 route는 저장소의 Noto Sans KR을 사용하므로 이미지 생성 시 외부 폰트 네트워크에 의존하지 않습니다. 하지만 전역 layout의 next/font/google까지 제거한 것은 아니므로, 프로젝트 전체 빌드가 완전히 오프라인에서 성공한다고 말할 수는 없습니다.

sitemap에는 현재 안전장치가 있습니다

게시글 수가 10,000개를 넘으면 현재 sitemap 구현의 최대 수집 범위에 걸립니다. 그 시점에는 sitemap index와 여러 sitemap으로 나누는 후속 작업이 필요합니다. 또한 DB 조회 오류가 발생하면 정적 URL 일부만 반환할 수 있으므로 운영 로그와 Search Console 상태를 함께 봐야 합니다.

환경변수 변경은 재배포와 한 쌍입니다

환경변수 이름과 값이 올바르더라도 배포가 새 값을 사용하지 않으면 사용자에게 보이는 HTML은 바뀌지 않습니다. 이번 사례의 가장 현실적인 교훈은 다음 한 문장입니다.

환경변수는 바꾸는 것으로 끝나지 않고, 그 값을 사용하는 환경에 다시 배포해야 한다.

마무리: SEO는 코드와 운영 연결이 함께 완성한다

이번 작업을 하며 SEO를 두 번 구현한 느낌이 들었습니다.

첫 번째 구현은 코드였습니다. 검색엔진과 소셜 서비스가 읽을 metadata, JSON-LD, sitemap, robots, OG 이미지를 만들었습니다.

두 번째 구현은 운영 연결이었습니다. 가비아 DNS에서 주소록을 만들고, Vercel 프로젝트와 custom domain을 연결하고, 환경변수를 바꾼 뒤 재배포하고, Google Search Console에 sitemap을 제출했습니다.

특히 사이트 주소를 코드 여러 곳에 하드코딩하지 않고 NEXT_PUBLIC_SITE_URLgetSiteUrl()을 기준으로 만든 것이 중요했습니다. 다음에 도메인을 바꾸더라도 코드 파일을 일일이 찾아 수정하는 대신, 환경별 주소를 설정하고 다시 배포하면 됩니다.

다만 이 구조도 “주소가 자동으로 바뀐다”는 마법은 아닙니다. 환경변수를 읽는 코드가 있어야 하고, 올바른 환경에 값을 넣어야 하며, 새 배포가 실제로 완료되어야 합니다. 마지막으로 운영 URL의 HTML과 Search Console에서 결과를 확인해야 합니다.

커밋 전에 스스로 설명해보기

다음 질문에 답할 수 있다면 이번 작업의 핵심을 이해한 것입니다.

  1. 왜 사이트 URL을 src/lib/site-config.ts와 환경변수로 관리하고, 여러 파일에 하드코딩하지 않아야 하나요?
  2. NEXT_PUBLIC_SITE_URL을 바꾼 뒤 재배포가 필요한 이유는 무엇인가요?
  3. kyy.co.kr, myblog.kyy.co.kr, myblog-navy-kappa.vercel.app은 각각 어떤 역할의 주소인가요?
  4. 가비아의 CNAME 레코드와 TXT 레코드는 각각 어떤 일을 하나요?
  5. robots.txt, sitemap.xml, Google Search Console은 서로 어떤 관계인가요?
  6. sitemap에서 “발견된 페이지”와 검색 결과에 “색인된 페이지”가 다른 이유는 무엇인가요?
  7. 기존 Vercel 주소를 redirect해야 하는 이유는 무엇인가요?
  8. 307 Temporary Redirect와 308 Permanent Redirect는 어떤 상황에서 구분해서 사용하나요?
  9. www를 사용하지 않아도 되는 이유와, 대신 공식 주소를 한 가지로 정해야 하는 이유는 무엇인가요?
  10. 로컬 OG 폰트가 해결하는 문제와 전역 next/font/google 때문에 여전히 남는 범위는 무엇인가요?
  11. 단위 테스트와 curl smoke test가 각각 증명하는 것은 무엇인가요?
  12. Search Console에 sitemap을 제출한 뒤에도 실제 색인과 검색 순위를 별도로 확인해야 하는 이유는 무엇인가요?

참고 자료

0개의 댓글
💬

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!