$ cd /blog

Vercel 무료 플랜에 비밀번호 걸기 — Edge Middleware로 사이트 통째 잠그는 법

Vercel의 배포 비밀번호 보호는 유료 플랜 기능입니다. 무료 플랜에서 개인용 사이트를 URL만으로 접근하지 못하게 막으려고 Edge Middleware로 키 게이트를 만들었습니다. 전체 코드와 실제로 밟은 함정 세 가지를 정리했습니다.


안녕하세요. Jay입니다!

개인용으로 만든 대시보드를 휴대폰에서도 보려고 Vercel에 올렸는데, 문제가 하나 있었습니다. 화면에 제 프로젝트 이름과 진행 상황이 그대로 나오는데 주소만 알면 누구나 들어올 수 있다는 거죠. Vercel에는 배포에 비밀번호를 거는 기능(Deployment Protection)이 있지만 유료 플랜 기능입니다. 취미 프로젝트에 요금제를 올리기는 아깝고, 그렇다고 로그인 시스템을 붙이자니 과하고요.

그래서 Edge Middleware로 사이트 전체에 키 게이트를 만들었습니다. 코드는 30줄이 안 되고, 실제로 쓰면서 밟은 함정이 세 개 있었는데 그것까지 같이 정리하겠습니다.

🔑 동작 방식 — 한 번만 키를 넣으면 1년 통과

흐름은 단순합니다.

  1. 키 없이 들어오면 401을 돌려줍니다.
  2. ?key=비밀값을 붙여 들어오면 쿠키를 심고 원래 주소로 넘겨줍니다.
  3. 그 뒤로는 쿠키만으로 통과합니다. 유효기간은 1년으로 잡았습니다.

즉 휴대폰에서 딱 한 번만 키가 붙은 주소로 접속하면, 그 뒤로는 홈 화면 아이콘을 눌러 바로 들어갈 수 있습니다.

📄 전체 코드

프로젝트 루트에 middleware.js(Next.js면 middleware.ts)를 두면 끝입니다.

export const config = { matcher: ['/((?!favicon|manifest|assets/icon).*)'] };

export default function middleware(req) {
  const KEY = process.env.JO_KEY;
  if (!KEY) return;                       // 키 미설정이면 게이트 없이 통과 (로컬 개발용)

  const url = new URL(req.url);
  const cookie = req.headers.get('cookie') || '';

  // 1) 이미 쿠키가 있으면 통과
  if (cookie.split(/;\s*/).includes(`site_key=${KEY}`)) return;

  // 2) ?key=... 로 들어오면 쿠키를 심고 리다이렉트
  if (url.searchParams.get('key') === KEY) {
    url.searchParams.delete('key');       // key만 제거, 나머지 쿼리는 보존
    const qs = url.searchParams.toString();
    return new Response(null, {
      status: 302,
      headers: {
        location: url.pathname + (qs ? '?' + qs : ''),
        'set-cookie': `site_key=${KEY}; Path=/; Max-Age=31536000; HttpOnly; Secure; SameSite=Lax`,
      },
    });
  }

  // 3) 그 외에는 차단
  return new Response('🔒 비공개 페이지입니다', {
    status: 401,
    headers: { 'content-type': 'text/plain; charset=utf-8' },
  });
}

키는 Vercel 프로젝트 설정의 Environment Variables에 넣습니다. 코드에 하드코딩하지 않아야 레포를 공개로 돌려도 안전하고, 키를 바꿀 때 재배포만 하면 됩니다.

⚠️ 실제로 밟은 함정 세 가지

1. 리다이렉트할 때 나머지 쿼리스트링이 날아갑니다

처음엔 키를 확인한 뒤 그냥 url.pathname으로 보냈습니다. 그랬더니 ?key=xxx&view=list 같이 다른 파라미터를 함께 붙여 들어온 경우 view=list가 통째로 사라지더라고요. 북마크가 매번 기본 화면으로 열려서 한참 헤맸습니다. 위 코드처럼 key만 지우고 남은 파라미터를 다시 붙여야 합니다.

2. 아이콘과 manifest는 게이트에서 빼야 합니다

iOS에서 홈 화면에 추가하면 웹 앱 manifest와 아이콘을 쿠키 없이 요청하는 경우가 있습니다. 이걸 401로 막으면 홈 화면 아이콘이 깨진 채로 추가됩니다. 그래서 matcher에서 favicon, manifest, 아이콘 경로를 제외했습니다. 어차피 그 파일들엔 민감한 정보가 없으니까요.

3. 키가 없을 때는 통과시켜야 개발이 편합니다

if (!KEY) return; 한 줄인데 꽤 중요합니다. 로컬 개발이나 미리보기 배포에서는 환경변수를 안 넣어두는 경우가 많은데, 이 분기가 없으면 개발 중에 자기 사이트에 못 들어가는 상황이 생깁니다.

🚧 이 방법의 한계

명확히 해둘 것이 있습니다. 이건 인증이 아니라 가림막입니다.

  • 키가 주소창에 노출되므로, 그 주소를 그대로 공유하면 키도 같이 전달됩니다.
  • 사용자별 권한 구분이 없습니다. 아는 사람은 모두 같은 권한입니다.
  • 브라우저 히스토리나 로그에 키가 남을 수 있습니다.

그래서 로그인이 필요한 서비스에는 쓰면 안 됩니다. 반대로 "검색엔진과 우연한 방문자만 막으면 되는 개인용 페이지"에는 비용 0원으로 충분한 수준입니다. 저처럼 나만 보는 대시보드, 가족끼리 보는 사진 페이지, 클라이언트에게만 보여주는 시안 사이트 정도가 적당합니다.

민감도가 조금 더 높다면 쿠키 값에 키를 그대로 넣지 말고 해시를 넣거나, 키에 만료 시각을 서명해 넣는 방식으로 올릴 수 있습니다. 다만 그 지점을 넘어간다면 그냥 제대로 된 인증을 붙이는 편이 낫습니다.

💡 Jay의 한 줄 — "유료 기능 앞에서 한 번은 직접 만들어보세요"

Deployment Protection 결제 페이지를 보다가 "이거 미들웨어로 되지 않나?"라는 생각이 들어 30분 만에 만든 게 이 코드입니다. 물론 유료 기능이 훨씬 견고하고, 팀 단위로 쓴다면 당연히 그쪽이 맞습니다. 그래도 직접 만들어보니 내 사이트가 요청을 어떤 순서로 처리하는지를 확실히 알게 됐어요. 특히 iOS가 manifest를 쿠키 없이 요청한다는 건 만들어보지 않았으면 평생 몰랐을 겁니다.

결론

Vercel 무료 플랜에서 사이트 전체를 잠그고 싶다면, 미들웨어 파일 하나 + 환경변수 하나면 됩니다. 쿼리스트링 보존, 아이콘 경로 제외, 키 미설정 시 통과 — 이 세 가지만 챙기면 실제로 쓸 만합니다. 다만 이건 가림막이라는 점을 잊지 마세요.

다음에도 유익한 포스팅으로 찾아오겠습니다. 감사합니다!

📚 참고 / 출처

#Vercel#Edge Middleware#Next.js#보안#쿠키

$ ls related/

더 많은 IT·AI 활용법이 궁금하다면?

매주 새로운 실전 가이드가 업데이트됩니다.

전체 글 보기