교차 출처 서비스 워커 - 외부 가져오기 실험

배경

서비스 워커는 웹 개발자가 웹 애플리케이션에서 실행하는 네트워크 요청에 응답할 수 있는 기능을 제공하므로 오프라인 상태에서도 계속 작업하고, 라이파이(lie-fi)를 방지하며, stale-while-revalidate와 같은 복잡한 캐시 상호작용을 구현할 수 있습니다. 하지만 서비스 워커는 이전부터 특정 출처에 연결되어 있었습니다. 웹 앱 소유자는 웹 앱에서 실행하는 모든 네트워크 요청을 가로채는 서비스 워커를 작성하고 배포할 책임이 있습니다. 이 모델에서 각 서비스 워커는 서드 파티 API 또는 웹 글꼴과 같은 교차 출처 요청도 처리합니다.

API, 웹 글꼴 또는 기타 일반적으로 사용되는 서비스의 서드 파티 제공업체가 다른 출처에서 출처로 보낸 요청을 처리할 수 있는 자체 서비스 워커를 배포할 수 있다면 어떻게 될까요? 제공업체는 자체 맞춤 네트워킹 로직을 구현하고 응답을 저장하기 위해 단일의 공신력 있는 캐시 인스턴스를 활용할 수 있습니다. 이제 외부 가져오기 덕분에 이러한 유형의 서드 파티 서비스 워커 배포가 가능해졌습니다.

외부 가져오기를 구현하는 서비스 워커를 배포하는 것은 브라우저의 HTTPS 요청을 통해 액세스되는 서비스의 모든 제공업체에 적합합니다. 브라우저가 공통 리소스 캐시를 활용할 수 있는 네트워크 독립형 버전의 서비스를 제공할 수 있는 시나리오를 생각해 보세요. 이 기능의 이점을 누릴 수 있는 서비스에는 다음이 포함되나 이에 국한되지 않습니다.

  • RESTful 인터페이스를 사용하는 API 제공업체
  • 웹 글꼴 제공업체
  • 애널리틱스 제공업체
  • 이미지 호스팅 제공업체
  • 일반 콘텐츠 전송 네트워크

예를 들어 분석 제공업체라고 가정해 보겠습니다. 외부 가져오기 서비스 워커를 배포하면 사용자가 오프라인 상태일 때 실패한 모든 서비스 요청이 연결이 복원되면 대기열에 추가되고 재생되도록 할 수 있습니다. 서비스의 클라이언트는 퍼스트 파티 서비스 워커를 통해 유사한 동작을 구현할 수 있지만, 모든 클라이언트가 서비스의 맞춤형 로직을 작성하도록 요구하는 것은 개발자가 배포하는 공유된 외부 가져오기 서비스 워커를 사용하는 것만큼 확장 가능한 것은 아닙니다.

기본 요건

오리진 트라이얼 토큰

외부 가져오기는 아직 실험 단계로 간주됩니다. 브라우저 공급업체가 이 디자인을 완전히 지정하고 동의하기 전에 조기에 구현하지 않도록 하기 위해 Chrome 54에서 출처 체험판으로 구현되었습니다. 외부 가져오기가 실험 단계인 동안 호스팅하는 서비스에서 이 새로운 기능을 사용하려면 서비스의 특정 출처로 범위가 지정된 토큰을 요청해야 합니다. 토큰은 외부 가져오기를 통해 처리하려는 리소스에 대한 모든 교차 출처 요청과 서비스 워커 JavaScript 리소스에 대한 응답에 HTTP 응답 헤더로 포함되어야 합니다.

Origin-Trial: token_obtained_from_signup

무료 체험은 2017년 3월에 종료됩니다. 그때까지는 이 기능을 안정화하는 데 필요한 변경사항을 파악하고 기본적으로 사용 설정할 수 있기를 바랍니다. 그때까지 외부 가져오기가 기본적으로 사용 설정되지 않으면 기존 오리진 트라이얼 토큰에 연결된 기능이 작동을 멈춥니다.

공식 오리진 트라이얼 토큰을 등록하기 전에 외부 가져오기를 실험할 수 있도록 chrome://flags/#enable-experimental-web-platform-features로 이동하고 '실험용 웹 플랫폼 기능' 플래그를 사용 설정하여 로컬 컴퓨터에 대한 Chrome의 요구사항을 우회할 수 있습니다. 로컬 실험에 사용하려는 모든 Chrome 인스턴스에서 이 작업을 실행해야 하지만, 오리진 체험판 토큰을 사용하면 모든 Chrome 사용자가 이 기능을 사용할 수 있습니다.

HTTPS

모든 서비스 워커 배포와 마찬가지로 리소스와 서비스 워커 스크립트를 모두 제공하는 데 사용하는 웹 서버는 HTTPS를 통해 액세스해야 합니다. 또한 외부 가져오기 가로채기는 보안 출처에서 호스팅되는 페이지에서 발생한 요청에만 적용되므로, 서비스 클라이언트는 HTTPS를 사용하여 외부 가져오기 구현을 활용해야 합니다.

외부 가져오기 사용

기본 요건을 완료했으므로 외부 가져오기 서비스 작업자를 시작하고 실행하는 데 필요한 기술 세부정보를 자세히 살펴보겠습니다.

서비스 워커 등록

가장 먼저 마주하게 될 과제는 서비스 워커를 등록하는 방법입니다. 이전에 서비스 워커를 사용해 본 적이 있다면 다음 사항에 익숙할 것입니다.

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

퍼스트 파티 서비스 워커 등록을 위한 이 JavaScript 코드는 사용자가 개발자가 제어하는 URL로 이동할 때 트리거되는 웹 앱 컨텍스트에서 적합합니다. 하지만 브라우저와 서버 간의 유일한 상호작용이 전체 탐색이 아닌 특정 하위 리소스를 요청하는 경우 서드 파티 서비스 워커를 등록하는 데는 적합하지 않습니다. 브라우저가 개발자가 유지 관리하는 CDN 서버에서 이미지를 요청하는 경우 해당 JavaScript 스니펫을 응답 앞에 추가하고 실행될 것으로 기대할 수 없습니다. 일반 JavaScript 실행 컨텍스트 외부의 다른 서비스 워커 등록 방법이 필요합니다.

솔루션은 서버가 모든 응답에 포함할 수 있는 HTTP 헤더 형식입니다.

Link: </service-worker.js>; rel="serviceworker"; scope="/"

이 헤더 예시를 구성요소로 분류해 보겠습니다. 각 구성요소는 ; 문자로 구분됩니다.

  • </service-worker.js>은 필수이며 서비스 워커 파일의 경로를 지정하는 데 사용됩니다 (/service-worker.js를 스크립트의 적절한 경로로 바꿈). 이는 navigator.serviceWorker.register()에 첫 번째 매개변수로 전달되는 scriptURL 문자열에 직접적으로 해당합니다. 값은 <> 문자로 묶여야 합니다 (Link 헤더 사양에 따라). 절대 URL이 아닌 상대 URL이 제공된 경우 응답 위치를 기준으로 상대로 해석됩니다.
  • rel="serviceworker"도 필수이며 맞춤설정할 필요 없이 포함되어야 합니다.
  • scope=/는 선택적 범위 선언으로, navigator.serviceWorker.register()의 두 번째 매개변수로 전달할 수 있는 options.scope 문자열과 같습니다. 대부분의 사용 사례에서는 기본 범위를 사용해도 괜찮습니다. 따라서 필요하지 않다고 판단되면 언제든지 범위를 생략해도 됩니다. Service-Worker-Allowed 헤더를 통해 이러한 제한을 완화할 수 있는 기능과 함께 최대 허용 범위에 대한 동일한 제한사항이 Link 헤더 등록에 적용됩니다.

'기존' 서비스 워커 등록과 마찬가지로 Link 헤더를 사용하면 등록된 범위에 대해 실행되는 다음 요청에 사용할 서비스 워커가 설치됩니다. 특수 헤더가 포함된 응답 본문은 있는 그대로 사용되며 외부 서비스 작업자가 설치를 완료할 때까지 기다리지 않고 페이지에서 즉시 사용할 수 있습니다.

외부 가져오기는 현재 출처 체험판으로 구현되므로 링크 응답 헤더와 함께 유효한 Origin-Trial 헤더도 포함해야 합니다. 외부 가져오기 서비스 워커를 등록하기 위해 추가해야 하는 최소 응답 헤더 집합은 다음과 같습니다.

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

등록 디버깅

개발 중에 외부 가져오기 서비스 워커가 제대로 설치되어 요청을 처리하는지 확인해야 할 수 있습니다. Chrome의 개발자 도구에서 몇 가지 사항을 확인하여 정상적으로 작동하는지 확인할 수 있습니다.

적절한 응답 헤더가 전송되고 있나요?

외부 가져오기 서비스 워커를 등록하려면 이 게시물 앞부분에 설명된 대로 도메인에 호스팅된 리소스에 대한 응답에 Link 헤더를 설정해야 합니다. 오리진 체험판 기간 동안 chrome://flags/#enable-experimental-web-platform-features가 설정되어 있지 않은 경우 Origin-Trial 응답 헤더도 설정해야 합니다. DevTools의 Network 패널 항목을 살펴보면 웹 서버가 이러한 헤더를 설정하고 있는지 확인할 수 있습니다.

네트워크 패널에 표시된 헤더

외부 가져오기 서비스 워커가 제대로 등록되어 있나요?

DevTools의 Application 패널에서 서비스 워커의 전체 목록을 확인하여 범위를 비롯한 기본 서비스 워커 등록을 확인할 수도 있습니다. 기본적으로 현재 출처의 서비스 워커만 표시되므로 '모두 표시' 옵션을 선택해야 합니다.

애플리케이션 패널의 외부 가져오기 서비스 워커

install 이벤트 핸들러

이제 서드 파티 서비스 워커를 등록했으므로 다른 서비스 워커와 마찬가지로 installactivate 이벤트에 응답할 수 있습니다. 이러한 이벤트를 활용하여 예를 들어 install 이벤트 중에 필요한 리소스로 캐시를 채우거나 activate 이벤트에서 오래된 캐시를 정리할 수 있습니다.

일반적인 install 이벤트 캐싱 활동 외에도 서드 파티 서비스 워커의 install 이벤트 핸들러 내에서 필요한 추가 단계가 있습니다. 코드는 다음 예와 같이 registerForeignFetch()를 호출해야 합니다.

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

두 가지 구성 옵션이 있으며 모두 필수입니다.

  • scopes는 하나 이상의 문자열 배열을 사용하며, 각 문자열은 foreignfetch 이벤트를 트리거하는 요청의 범위를 나타냅니다. 그런데 서비스 워커 등록 중에 이미 범위를 정의했습니다라고 생각하실 수도 있습니다. 맞습니다. 전반적인 범위는 여전히 관련이 있습니다. 여기에서 지정하는 각 범위는 서비스 워커의 전반적인 범위와 같거나 그 하위 범위여야 합니다. 여기서 추가로 범위 제한을 적용하면 자체 사이트에서 이루어진 요청의 경우 퍼스트 파티 fetch 이벤트와 다른 도메인에서 이루어진 요청의 경우 서드 파티 foreignfetch 이벤트를 모두 처리할 수 있는 범용 서비스 워커를 배포하고 더 큰 범위의 하위 집합만 foreignfetch을 트리거해야 한다고 명시할 수 있습니다. 실제로 서드 파티 foreignfetch 이벤트만 처리하는 전용 서비스 워커를 배포하는 경우 서비스 워커의 전체 범위와 동일한 명시적 단일 범위만 사용하면 됩니다. 위의 예에서는 self.registration.scope 값을 사용하여 이를 수행합니다.
  • origins는 하나 이상의 문자열 배열도 가져오며, 특정 도메인의 요청에만 응답하도록 foreignfetch 핸들러를 제한할 수 있습니다. 예를 들어 'https://example.com'을 명시적으로 허용하는 경우 https://example.com/path/to/page.html에 호스팅된 페이지에서 외부 가져오기 범위에서 제공되는 리소스에 대해 요청하면 외부 가져오기 핸들러가 트리거되지만 https://random-domain.com/path/to/page.html에서 이루어진 요청은 핸들러를 트리거하지 않습니다. 원격 출처의 하위 집합에 대해서만 외부 가져오기 로직을 트리거할 특별한 이유가 없다면 배열에 '*'만 유일한 값으로 지정하면 모든 출처가 허용됩니다.

foreignfetch 이벤트 핸들러

이제 서드 파티 서비스 워커를 설치했고 registerForeignFetch()를 통해 구성되었으므로 외부 가져오기 범위에 속하는 서버에 대한 교차 출처 하위 리소스 요청을 가로챌 수 있습니다.

기존의 퍼스트 파티 서비스 워커에서 각 요청은 서비스 워커가 응답할 수 있는 fetch 이벤트를 트리거합니다. 서드 파티 서비스 워커는 foreignfetch라는 약간 다른 이벤트를 처리할 수 있습니다. 개념적으로 두 이벤트는 매우 유사하며, 수신되는 요청을 검사하고 원하는 경우 respondWith()를 통해 응답을 제공할 수 있습니다.

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

개념적으로는 유사하지만 ForeignFetchEvent에서 respondWith()를 호출할 때는 실제로 몇 가지 차이점이 있습니다. FetchEvent로 하는 것처럼 respondWith()Response(또는 Response로 확인되는 Promise)를 제공하는 대신 특정 속성이 있는 객체로 확인되는 PromiseForeignFetchEventrespondWith()에 전달해야 합니다.

  • response는 필수이며 요청을 한 클라이언트에 반환될 Response 객체로 설정해야 합니다. 유효한 Response가 아닌 항목을 제공하면 클라이언트의 요청이 네트워크 오류와 함께 종료됩니다. fetch 이벤트 핸들러 내에서 respondWith()를 호출할 때와 달리, Response로 확인되는 Promise가 아닌 Response를 여기에 반드시 제공해야 합니다. 프로미스 체인을 통해 응답을 구성하고 이 체인을 foreignfetchrespondWith()에 매개변수로 전달할 수 있지만 체인은 Response 객체로 설정된 response 속성이 포함된 객체로 확인되어야 합니다. 위의 코드 샘플에서 이를 확인할 수 있습니다.
  • origin는 선택사항이며 반환된 응답이 불투명인지 확인하는 데 사용됩니다. 이를 생략하면 응답이 불투명해지고 클라이언트는 응답의 본문과 헤더에 제한적으로 액세스할 수 있습니다. mode: 'cors'로 요청된 경우 불투명 응답을 반환하면 오류로 처리됩니다. 그러나 원격 클라이언트의 출처(event.origin를 통해 가져올 수 있음)와 동일한 문자열 값을 지정하면 클라이언트에 CORS 지원 응답을 제공하도록 명시적으로 선택하는 것입니다.
  • headers도 선택사항이며 origin도 지정하고 CORS 응답을 반환하는 경우에만 유용합니다. 기본적으로 CORS 허용 목록에 있는 응답 헤더 목록의 헤더만 응답에 포함됩니다. 반환되는 항목을 추가로 필터링해야 하는 경우 headers는 하나 이상의 헤더 이름 목록을 가져와 응답에 노출할 헤더의 허용 목록으로 사용합니다. 이렇게 하면 CORS를 선택하면서도 민감할 수 있는 응답 헤더가 원격 클라이언트에 직접 노출되는 것을 방지할 수 있습니다.

foreignfetch 핸들러가 실행되면 서비스 워커를 호스팅하는 출처의 모든 사용자 인증 정보와 대기 권한에 액세스할 수 있습니다. 외부 가져오기 지원 서비스 워커를 배포하는 개발자는 이러한 사용자 인증 정보로 인해 사용할 수 없는 권한이 있는 응답 데이터를 유출하지 않도록 할 책임이 있습니다. CORS 응답에 대한 선택을 요구하는 것은 의도치 않은 노출을 제한하는 한 가지 방법이지만 개발자는 다음을 통해 foreignfetch 핸들러 내에서 암시된 사용자 인증 정보를 사용하지 않는 fetch() 요청을 명시적으로 실행할 수 있습니다.

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

클라이언트 고려사항

외부 가져오기 서비스 워커가 서비스 클라이언트의 요청을 처리하는 방식에 영향을 주는 몇 가지 추가 고려사항이 있습니다.

자체 퍼스트 파티 서비스 워커가 있는 클라이언트

서비스의 일부 클라이언트에는 이미 자체 퍼스트 파티 서비스 워커가 있어 웹 앱에서 발생하는 요청을 처리하고 있을 수 있습니다. 서드 파티 외부 가져오기 서비스 워커에게는 어떤 의미가 있나요?

퍼스트 파티 서비스 워커의 fetch 핸들러는 요청을 포함하는 범위로 foreignfetch가 사용 설정된 서드 파티 서비스 워커가 있더라도 웹 앱에서 실행하는 모든 요청에 응답할 수 있는 첫 번째 기회를 갖습니다. 하지만 퍼스트 파티 서비스 워커가 있는 클라이언트는 여전히 외부 가져오기 서비스 워커를 활용할 수 있습니다.

퍼스트 파티 서비스 워커 내에서 fetch()를 사용하여 교차 출처 리소스를 검색하면 적절한 외부 가져오기 서비스 워커가 트리거됩니다. 즉, 다음과 같은 코드가 foreignfetch 핸들러를 활용할 수 있습니다.

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

마찬가지로 퍼스트 파티 가져오기 핸들러가 있지만 교차 출처 리소스에 대한 요청을 처리할 때 event.respondWith()를 호출하지 않으면 요청이 자동으로 foreignfetch 핸들러로 '전달'됩니다.

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

퍼스트 파티 fetch 핸들러가 event.respondWith()를 호출하지만 외부 가져오기 범위에서 리소스를 요청하기 위해 fetch()를 사용하지 않는 경우 외부 가져오기 서비스 워커는 요청을 처리할 기회를 얻지 못합니다.

자체 서비스 워커가 없는 클라이언트

서드 파티 서비스에 요청하는 모든 클라이언트는 아직 자체 서비스 워커를 사용하지 않더라도 서비스에서 외부 가져오기 서비스 워커를 배포하면 이점을 얻을 수 있습니다. 외부 가져오기 서비스 워커를 사용하도록 선택하려면 클라이언트가 이를 지원하는 브라우저를 사용하고 있는 한 별도로 취해야 할 조치는 없습니다. 즉, 외부 가져오기 서비스 워커를 배포하면 맞춤 요청 로직과 공유 캐시가 추가 단계 없이 많은 서비스 클라이언트에게 즉시 도움이 됩니다.

종합: 클라이언트가 응답을 찾는 위치

위 정보를 고려하여 클라이언트가 교차 출처 요청에 대한 응답을 찾는 데 사용할 소스의 계층 구조를 조합할 수 있습니다.

  1. 퍼스트 파티 서비스 워커의 fetch 핸들러 (있는 경우)
  2. 서드 파티 서비스 워커의 foreignfetch 핸들러(있는 경우 교차 출처 요청에만 해당)
  3. 브라우저의 HTTP 캐시(최신 응답이 있는 경우)
  4. 네트워크

브라우저는 위에서부터 시작하며, 서비스 워커 구현에 따라 응답 소스를 찾을 때까지 목록 아래로 계속 진행합니다.

자세히 알아보기

최신 정보 확인

Chrome의 외부 가져오기 오리진 트라이얼 구현은 개발자의 의견을 반영하면서 변경될 수 있습니다. 인라인 변경사항을 통해 이 게시물을 최신 상태로 유지하고 변경사항이 있을 때마다 아래에 구체적인 변경사항을 기록할 예정입니다. @chromiumdev 트위터 계정을 통해 주요 변경사항에 대한 정보도 공유할 예정입니다.