Chrome Immediate UI 적용법, 로그인 흐름 5단계

Chrome Immediate UI를 기존 로그인 링크에 붙이면 페이지를 떠나지 않고 패스키로 로그인할 수 있다. 지원 확인부터 서버 검증, 기존 로그인 화면으로 돌아가는 방법까지 5단계로 짚었다.

Chrome패스키WebAuthn인증
--
Chrome Immediate UI 적용법, 로그인 흐름 5단계

Chrome Immediate UI를 로그인 링크에 붙이면 사용자는 보던 페이지에서 계정을 고를 수 있다.

하지만 기기에 쓸 수 있는 패스키가 없거나 브라우저가 기능을 지원하지 않으면 기존 로그인 화면으로 자연스럽게 넘어가야 한다.

Chrome은 2026년 9월 30일 공개한 UX 가이드에서 바로 이 적용 위치와 피해야 할 위치를 정리했다.
이 글은 헤더의 로그인 링크를 예로 들어 지원 확인, 패스키 요청, 서버 검증, 화면 갱신, 폴백을 5단계로 연결한다.

1. Chrome Immediate UI를 로그인 화면 앞에 놓기

평범한 로그인 링크는 클릭하면 /login으로 이동한다. Immediate UI를 붙인 링크는 그 이동 직전에 navigator.credentials.get({ uiMode: 'immediate' })을 시도한다. 브라우저가 같은 기기에서 바로 쓸 수 있는 자격 증명을 찾으면 계정 선택 창을 띄운다. 찾지 못하면 별도 창 없이 빠르게 실패하므로 사이트는 원래 링크로 이동시키면 된다.

Chrome은 Chrome 149에서 이 모드를 패스키와 저장된 비밀번호에 도입했다. 9월 가이드는 헤더 로그인 링크, 체크아웃 진입, 북마크처럼 로그인이 필요한 작은 행동을 적용 사례로 든다. 세 사례 모두 사용자가 클릭으로 로그인 의사를 드러낸 뒤, 다음 화면으로 넘어가기 전이라는 공통점이 있다.

이 글의 코드는 그중 패스키 경로만 다룬다. 비밀번호까지 한 창에서 받으려면 password: true를 추가하고 서버의 비밀번호 인증 경로도 별도로 연결해야 한다.

로그인 폼의 자동 완성과는 역할이 다르다. Conditional UI는 입력창에 초점을 맞췄을 때 패스키를 자동 완성 후보로 보여준다. Immediate UI는 링크나 버튼 클릭에 맞춰 계정 선택을 바로 시도한다. 전용 로그인 화면에는 Conditional UI와 일반 패스키 버튼을 둔다. 그 화면에 도착하기 전 링크에 Immediate UI를 붙이면 된다.

2. 브라우저 지원과 WebAuthn 서버를 준비하기

2026년 10월 기준으로 Immediate UI를 모든 브라우저에서 쓸 수 있다고 가정하면 안 된다. Chrome의 9월 가이드는 Chrome 및 Chromium 계열 지원을 안내한다. Safari나 Firefox에서는 원래 로그인 링크가 그대로 동작하도록 두는 편이 안전하다. 지원 여부는 브라우저 이름 대신 PublicKeyCredential.getClientCapabilities()가 돌려주는 immediateGet으로 확인한다.

패스키가 이미 등록되어 있어야 이 경로로 로그인할 수 있다. 서버는 로그인 시도마다 새로운 challenge를 만들고 세션과 연결해 보관한다. 응답이 돌아오면 challenge, origin, RP ID, 사용자 확인 조건, 서명을 검증한 뒤 로그인 세션을 만든다.

사이트에 WebAuthn 로그인이 있다면 같은 검증 코드를 재사용한다. Immediate UI가 바꾸는 것은 인증 결과를 확인하는 규칙이 아니라, 브라우저가 자격 증명을 제안하는 시점이다.

다음 예제는 두 서버 API가 이미 있다고 가정한다. GET /api/passkeys/authentication-options는 { publicKey, expiresAt } JSON을 반환한다. publicKey에는 base64url로 인코딩한 challenge와 RP ID가 들어 있다. expiresAt은 challenge 만료 시각을 밀리초 단위 Unix 시간으로 담는다.

POST /api/passkeys/authentication-verify는 WebAuthn 응답을 검증하고 세션을 발급한다. 실제 경로와 응답 형식은 사용 중인 인증 서버에 맞춰야 한다.

3. 클릭 직후 패스키 선택 창을 열기

Immediate UI 요청에는 사용자의 클릭이나 탭이 필요하다. 클릭 핸들러에서 서버 응답을 기다린 뒤 get()을 부르면 사용자 활성화 조건을 놓칠 수 있다. 예제는 화면이 준비될 때 challenge를 미리 받아 둔 뒤 클릭 핸들러 안에서 바로 get()을 호출한다. 옵션을 받지 못했거나 만료됐다면 링크를 가로채지 않고 /login으로 이동한다.

<a id="login-link" href="/login">로그인</a>
<script type="module">
  const link = document.querySelector('#login-link');
  let prepared;

  try {
    if (window.PublicKeyCredential?.getClientCapabilities) {
      const capabilities = await PublicKeyCredential.getClientCapabilities();
      if (capabilities.immediateGet) {
        const response = await fetch('/api/passkeys/authentication-options');
        if (response.ok) {
          const { publicKey, expiresAt } = await response.json();
          if (Number.isFinite(expiresAt)) {
            prepared = {
              options: PublicKeyCredential.parseRequestOptionsFromJSON(publicKey),
              expiresAt,
            };
          }
        }
      }
    }
  } catch {
    // 준비에 실패해도 링크는 평소대로 동작한다.
  }

  link.addEventListener('click', async (event) => {
    if (!prepared || Date.now() >= prepared.expiresAt) return;
    event.preventDefault();
    const { options } = prepared;
    prepared = undefined; // 같은 challenge로 다시 시도하지 않는다.

    try {
      const credential = await navigator.credentials.get({
        publicKey: options,
        uiMode: 'immediate',
      });
      if (!credential) throw new Error('패스키 응답 없음');

      const response = await fetch('/api/passkeys/authentication-verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify(credential),
      });
      if (!response.ok) throw new Error('서버 인증 실패');

      link.textContent = '내 계정';
      link.href = '/account';
    } catch {
      window.location.assign(link.href);
    }
  });
</script>

PublicKeyCredential.parseRequestOptionsFromJSON()은 서버가 보낸 JSON을 WebAuthn 요청 옵션으로 바꾼다. JSON.stringify(credential)은 브라우저의 toJSON()을 이용해 인증 응답을 전송 가능한 형태로 만든다. 예제는 단일 로그인 링크의 최소 흐름이다.

앱의 다른 영역도 로그인 상태를 표시한다면 서버 검증에 성공한 뒤 인증 상태를 다시 읽어 화면을 갱신해야 한다. 페이지를 강제로 새로고침하지 않으면 스크롤 위치와 작성 중인 입력값도 유지할 수 있다.

4. 브라우저 선택 뒤 서버 검증을 끝내기

계정 선택 창에서 패스키를 골랐다고 로그인에 성공한 것은 아니다.

브라우저 응답은 반드시 서버로 보내야 한다.

서버는 미리 저장한 challenge와 받은 응답을 대조하고 요청한 사이트의 origin과 RP ID가 맞는지 확인한다.
사용자 확인 플래그와 서명도 검사한 다음에 세션을 발급한다.

Google의 서버 구현 가이드는 직접 검증 로직을 짜기보다 검증된 WebAuthn 라이브러리를 사용하라고 권한다.

프론트엔드에서는 서버가 성공을 알리기 전까지 로그인 상태를 바꾸지 않는다. 위 코드는 응답이 성공한 뒤에만 헤더 링크를 내 계정으로 바꾼다. SPA에서 사용자 정보를 캐시한다면 같은 시점에 인증 쿼리를 다시 불러와야 한다.

북마크처럼 로그인 전에 누른 행동이 있었다면, 서버 인증이 끝난 뒤 해당 요청을 이어서 처리한다. 화면만 성공으로 바꿨다가 서버가 거절하면 사용자는 자신의 작업이 저장됐다고 오해할 수 있다.

서버 API의 세션 쿠키와 CSRF 방어는 기존 로그인 경로의 정책을 따른다. 예제의 엔드포인트 이름과 응답 형식은 설명을 위한 계약이며 서버 검증을 생략할 수 있다는 뜻이 아니다. challenge가 만료되거나 한 번 사용되면 서버도 재사용을 거부해야 한다. 같은 링크로 다시 시도할 때는 새 challenge를 받아야 한다.

5. 자격 증명이 없을 때 기존 로그인으로 돌아가기

Chrome 문서는 로컬 자격 증명이 없거나 사용자가 계정 창을 닫으면 NotAllowedError가 날 수 있다고 설명한다.

시크릿 모드 요청도 같은 오류로 끝난다.

이 오류만 보고 원인을 단정할 수 없으므로 사용자에게 기술적인 오류명을 보여줄 필요는 없다. 기존 로그인 화면으로 보내고 이메일·비밀번호, 휴대전화의 패스키, 보안 키 등 서비스가 이미 지원하는 수단을 고르게 한다.

전용 로그인 화면의 패스키 버튼에는 Immediate UI를 쓰지 않는 편이 좋다. Chrome의 UX 가이드는 이 모드가 다른 기기의 패스키를 쓸 QR 코드와 보안 키 경로를 생략한다고 설명한다. 사용자가 마지막으로 도착한 로그인 화면에서까지 빠른 로컬 검색만 반복하면 더 진행할 길이 없어진다. 그 화면에는 일반 WebAuthn 호출과 Conditional UI 같은 기존 선택지를 남긴다.

출시 전에는 성공 사례만 시험하지 말아야 한다.

지원되는 Chrome에서 등록된 패스키를 고르는 흐름, 자격 증명이 없는 기기, 창을 닫은 경우를 각각 확인한다.

Safari·Firefox와 시크릿 모드에서도 /login 링크가 열려야 한다. challenge 만료와 서버 검증 실패 때도 사용자가 막히지 않는지 확인한다. 실제 서비스에 붙일 때는 원래 화면에서 로그인한 사용자가 이어서 하려던 행동까지 끝낼 수 있는지 살펴야 한다.

한 가지 문서 차이도 확인할 만하다. 2026년 9월 UX 가이드는 민감한 작업의 재인증에 allowCredentials를 지정하라고 한다.

그러나 5월 API 문서는 비어 있지 않은 allowCredentials 요청이 NotAllowedError로 끝난다고 설명한다.

공식 문서끼리 다르므로 이 글의 예제는 특정 계정의 재인증에 쓰지 않았다. 그 용도라면 대상 Chrome 버전에서 실제 동작을 확인해야 한다.

Chrome Immediate UI의 출발점은 기존 로그인 링크다.

지원되는 브라우저에서 준비된 패스키를 빠르게 제안하고 조건이 맞지 않으면 원래 화면으로 보내면 된다.

hrome Immediate UI 적용법, 로그인 흐름 5단계서버 검증을 그대로 유지하면서 현재 페이지의 맥락도 지키려면 성공 화면과 폴백 경로를 한 흐름으로 시험해야 한다.

참고 자료

관련 글

댓글

0/2000
Newsletter

이 글이 도움이 되셨나요?

새로운 글이 발행되면 이메일로 알려드립니다.

뉴스레터 구독하기