RSS
Next.js

비어 있는 Next.js 사이트맵: APP_URL 하나에 두 가지 뜻을 실은 스타터 템플릿

작성자
듀오랩스 대표·5분 읽기

형제 앱 하나의 운영 사이트맵을 열었더니 비어 있었습니다. <urlset> 태그만 있고 주소가 하나도 없었습니다. 코드에는 공개 경로 목록을 읽어 사이트맵을 만드는 sitemap.ts가 있었습니다. 목록도 비어 있지 않았습니다.

원인을 따라가 보니 이 앱 하나의 실수가 아니었습니다. 제가 새 앱을 만들 때 쓰는 공통 스타터 템플릿에 같은 구멍이 있었고, 그 템플릿으로 만든 앱은 모두 같은 구멍을 안고 태어날 수 있었습니다.

첫 번째 원인, 한 변수에 두 가지 뜻

사이트맵의 주소는 APP_URL 환경변수를 앞에 붙여 만들고 있었습니다. 운영 환경에는 이 값이 없었습니다. 그래서 주소를 만들 수 없는 항목이 빠지고 빈 사이트맵이 나왔습니다.

왜 값이 없었는지가 흥미로웠습니다. 스타터 템플릿의 설명은 APP_URL을 "비동기 작업 큐가 앱을 다시 부를 때 쓰는 주소"로 소개하고 있었습니다. 큐를 쓰지 않는 앱을 배포하는 사람은 이 값을 채울 이유가 없다고 읽습니다. 실제로 배포 설정에서도 빠져 있었습니다. 그런데 스타터의 robots.ts와 sitemap.ts는 같은 변수를 공개 주소로 쓰고 있었습니다.

한 변수에 두 가지 뜻이 실려 있었습니다. 큐의 콜백 주소와 검색엔진에 알려 줄 공개 주소입니다. 둘은 같은 값일 때가 많지만 같은 개념은 아닙니다. 그리고 설명에는 한쪽 뜻만 적혀 있었습니다.

두 번째 원인, 값을 넣었는데도 비어 있던 사이트맵

배포 설정에 APP_URL을 넣고 다시 배포했습니다. 사이트맵은 여전히 비어 있었습니다.

Next.js 앱 라우터에서 robots.ts와 sitemap.ts는 동적인 요소가 없으면 빌드할 때 한 번 만들어져 정적 파일로 굳습니다. 이 앱의 배포는 환경변수를 실행할 때만 넣고 빌드할 때는 넣지 않았습니다. 빌드 시점의 APP_URL은 여전히 비어 있었고, 그때 만들어진 빈 사이트맵이 그대로 나가고 있었습니다. 실행 환경에 값을 넣는 것으로는 이미 굳은 파일을 바꿀 수 없습니다.

고친 방법은 두 파일을 요청 때 만들도록 바꾸는 것이었습니다.

export const dynamic = "force-dynamic";

사이트맵은 자주 요청되는 파일이 아니라 요청 때 만드는 비용은 무시할 만합니다. 빌드 환경과 실행 환경의 값을 맞추는 것도 방법이지만, 두 환경이 어긋날 수 있다는 사실 자체를 없애는 쪽을 골랐습니다. 사이트맵이 언제 다시 만들어지는지에 대한 다른 함정은 Next.js 사이트맵과 메타데이터 라우트 캐싱에 따로 적었습니다.

템플릿의 구멍은 복제된다

두 원인 모두 이 앱이 만든 것이 아니었습니다. 스타터 템플릿의 robots.ts와 sitemap.ts가 같은 변수를 같은 방식으로 읽고 있었습니다. 템플릿으로 앱을 하나 만들 때마다 이 구멍도 하나씩 복사됩니다. 운이 좋은 앱은 큐를 써서 APP_URL을 채웠고 빌드 때도 값이 들어가서 문제가 드러나지 않았을 뿐입니다.

저는 이것이 템플릿이 가진 가장 불편한 성질이라고 봅니다. 템플릿은 좋은 것을 복제하는 도구이지만, 나쁜 것도 똑같은 효율로 복제합니다. 그리고 복제된 구멍은 앱마다 다른 모양으로 드러나서, 같은 원인이라는 것을 알아보기 어렵습니다.

공개 주소를 따로 두기

템플릿에서 할 일은 두 가지로 정했습니다. 첫째, 공개 주소를 SITE_URL이라는 별도 변수로 분리합니다. metadataBase, canonical, robots, 사이트맵은 모두 이 값을 기준으로 합니다. 큐의 콜백 주소는 APP_URL에 그대로 둡니다. 이름이 뜻을 하나만 말하면 "이 값은 우리 앱에 필요 없다"는 오해가 생기지 않습니다.

둘째, 배포 워크플로가 SITE_URL을 반드시 채우게 하고, 비어 있으면 배포를 멈추게 합니다. 사이트맵이 비어 있는 것은 에러가 아니라서 아무도 알려 주지 않습니다. 멈추게 하지 않으면 다음 앱도 조용히 빈 사이트맵으로 운영됩니다.

이미 만들어진 앱은 템플릿을 고쳐도 바뀌지 않습니다. 그래서 공개 호스트를 돌며 사이트맵이 비어 있지 않은지를 주기적으로 확인하는 점검을 함께 두기로 했습니다. 템플릿은 앞으로 태어날 앱을 지키고, 점검은 이미 태어난 앱을 지킵니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.