앞선 글에서 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.kr과 seongryung.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 화면에서 안내한 값을 사용해야 합니다.
일반적인 형태는 다음과 같습니다.
레코드
호스트
값의 역할
CNAME
myblog
myblog.kyy.co.kr을 Vercel이 안내한 대상에 연결
TXT
myblog 또는 안내된 호스트
도메인 소유권 또는 서비스 연결 확인
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 설정 때문에 잠시 이전 결과가 보일 수 있습니다.
따라서 다음 순서로 확인합니다.
가비아 DNS에 Vercel이 안내한 CNAME을 저장합니다.
Vercel Domains 화면에서 도메인 상태가 Valid Configuration인지 확인합니다.
https://myblog.kyy.co.kr을 직접 열어 사이트가 보이는지 확인합니다.
HTTPS 인증서가 정상으로 발급되었는지 브라우저 주소창에서 확인합니다.
3. Vercel 프로젝트에 custom domain을 추가합니다
Vercel의 기본 주소와 custom domain
Vercel의 기본 주소는 프로젝트를 빠르게 확인하기에 좋습니다. 하지만 운영 주소는 내가 정한 custom domain으로 통일하는 것이 좋습니다.
Vercel 프로젝트의 Domains 메뉴에서 다음 주소를 추가합니다.
text
myblog.kyy.co.kr
Vercel이 제시한 DNS 값을 가비아에 입력한 뒤, Vercel 화면에서 검증이 끝나면 custom domain이 프로젝트에 연결됩니다.
이 화면에서 위쪽의 myblog.kyy.co.kr은 실제 운영 주소이고, 아래쪽의 myblog-navy-kappa.vercel.app은 Vercel이 기본으로 제공한 주소입니다.
이런 상황에서는 방문자를 새 공식 주소로 이동시키는 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 같은 상세 경로가 끊기면 방문자와 검색엔진 모두 불편을 겪습니다.
개발자 도구의 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
이번 설정에서는 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.ts는 getSiteUrl()을 사용해 홈, 글 목록, 게시글 상세 URL을 만듭니다.
응답이 200이고 image/png로 반환되는지 확인합니다. 한글 제목이 들어간 게시글의 경우 실제 이미지를 열어 글자가 네모나 깨진 글꼴로 보이지 않는지도 확인합니다.
6) 프로젝트 자동 검사
코드 변경까지 포함된 작업이라면 프로젝트 루트에서 다음 검사를 실행합니다.
bash
npm run testnpx tsc --noEmitnpm run lint
npm run build
gitdiff--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을 연결했습니다.
Search Console에서 sitemap 처리 성공과 발견된 페이지 수를 확인했습니다.
기존 myblog-navy-kappa.vercel.app 주소를 새 도메인으로 redirect하도록 설정했습니다.
myblog의 여러 사이트 주소를 한 문자열로 하드코딩하지 않고 환경변수와 공통 helper를 통해 관리합니다.
12. 아직 보장하지 않는 것과 주의할 점
완료 목록이 있다고 해서 모든 문제가 해결된 것은 아닙니다.
검색 순위와 색인을 보장하지 않습니다
SEO 코드를 잘 작성하고 sitemap을 제출해도 Google이 모든 페이지를 색인하거나 특정 검색어에서 높은 순위를 보장하지는 않습니다. 콘텐츠 품질, 사이트 신뢰도, 중복 여부, 크롤링 일정, 검색어 경쟁 등 여러 요소가 함께 영향을 줍니다.
Naver Search Advisor는 아직 등록하지 않았습니다
이번 범위에서는 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의 정책이 다릅니다
/search는 noindex, 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_URL과 getSiteUrl()을 기준으로 만든 것이 중요했습니다. 다음에 도메인을 바꾸더라도 코드 파일을 일일이 찾아 수정하는 대신, 환경별 주소를 설정하고 다시 배포하면 됩니다.
다만 이 구조도 “주소가 자동으로 바뀐다”는 마법은 아닙니다. 환경변수를 읽는 코드가 있어야 하고, 올바른 환경에 값을 넣어야 하며, 새 배포가 실제로 완료되어야 합니다. 마지막으로 운영 URL의 HTML과 Search Console에서 결과를 확인해야 합니다.
커밋 전에 스스로 설명해보기
다음 질문에 답할 수 있다면 이번 작업의 핵심을 이해한 것입니다.
왜 사이트 URL을 src/lib/site-config.ts와 환경변수로 관리하고, 여러 파일에 하드코딩하지 않아야 하나요?
NEXT_PUBLIC_SITE_URL을 바꾼 뒤 재배포가 필요한 이유는 무엇인가요?
kyy.co.kr, myblog.kyy.co.kr, myblog-navy-kappa.vercel.app은 각각 어떤 역할의 주소인가요?
가비아의 CNAME 레코드와 TXT 레코드는 각각 어떤 일을 하나요?
robots.txt, sitemap.xml, Google Search Console은 서로 어떤 관계인가요?
sitemap에서 “발견된 페이지”와 검색 결과에 “색인된 페이지”가 다른 이유는 무엇인가요?
기존 Vercel 주소를 redirect해야 하는 이유는 무엇인가요?
307 Temporary Redirect와 308 Permanent Redirect는 어떤 상황에서 구분해서 사용하나요?
www를 사용하지 않아도 되는 이유와, 대신 공식 주소를 한 가지로 정해야 하는 이유는 무엇인가요?
로컬 OG 폰트가 해결하는 문제와 전역 next/font/google 때문에 여전히 남는 범위는 무엇인가요?
단위 테스트와 curl smoke test가 각각 증명하는 것은 무엇인가요?
Search Console에 sitemap을 제출한 뒤에도 실제 색인과 검색 순위를 별도로 확인해야 하는 이유는 무엇인가요?