모든 웹 개발자는 통제된 스테이징 환경에서 자신의 애플리케이션이 완벽하게 렌더링되는 것을 지켜볼 때의 그 기분을 알고 있습니다. 하지만 임베디드 위젯을 배포하는 순간, 그 안락함은 완전히 깨집니다. 당신은 더 이상 페이지의 설계자가 아닙니다. 당신은 자신이 소유하지 않은 DOM, 직접 작성하지 않은 CSS 캐스케이드, 그리고 당신에게 불리하게 작용할 수도 있는 런타임 환경에 React 애플리케이션을 주입하는 초대받지 않은 손님일 뿐입니다. Clanker Support 위젯을 구축하고 배포하면서, 우리는 표준 웹 개발 가정이 타인의 테마 안에서 코드가 실행되는 순간 무너진다는 것을 배웠습니다. 호스트 사이트는 글꼴 크기를 초기화하거나, 빈 div를 숨기거나, 설정을 읽기도 전에 무효화하는 스크립트 라이프사이클을 강제할 수도 있습니다. 다음은 우리가 실전의 피와 땀으로 써 내려간 방어 규칙들입니다.
단일 파일, 단일 실패 모드 (One File, One Failure Mode)
현대적인 번들러는 코드 분할(code splitting)과 동적 임포트(dynamic imports)로 유혹합니다. 이를 거부하십시오. 임베디드 위젯은 반드시 단일 파일 형태의 즉시 실행 함수 표현식(IIFE)으로 배포되어야 합니다. 고객이 자신의 템플릿에 스크립트 태그를 복사할 때, 그들은 단 한 번의 네트워크 요청을 기대합니다. 만약 번들이 무거운 파싱 라이브러리나 언어 모델 청크를 지연 로딩(lazy-load)하려고 하면, fetch가 조용히 실패할 수 있습니다. 호스트에 엄격한 CSP(Content Security Policy)가 있거나, 공격적인 광고 차단기, 또는 publicPath 가정과 일치하지 않는 CDN 경로가 있을 수 있기 때문입니다. 모든 것을 하나의 IIFE로 강제함으로써, 2차 청크 로딩의 불확실성을 제거할 수 있습니다. 만약 의존성 라이브러리가 내부적으로 지연 로딩을 고집한다면, 빌드 타임에 가벼운 스텁(stub)으로 별칭(alias)을 지정하십시오. 그 결과물은 단일 아티팩트이자 단일 실패 모드가 되며, 고객 사이트 관리자가 깨진 채팅 버블 스크린샷을 이메일로 보냈을 때 훨씬 쉽게 디버깅할 수 있습니다.
Shadow DOM도 유출됩니다
개발자들은 종종 Shadow DOM을 난공불락의 요새처럼 취급합니다. Shadow DOM이 호스트 페이지의 CSS로부터 셀렉터를 격리해 주는 것은 맞지만, 상속(inheritance)까지 격리하지는 못합니다. font-family, line-height, color, text-align과 같은 속성들은 경계가 없는 것처럼 Shadow tree로 흘러 들어갑니다. 전역적으로 font-family: "Comic Sans MS"가 선언된 Shopify 스토어는, 루트 요소에서 모든 상속 가능한 속성을 명시적으로 고정하지 않는 한 당신이 정성껏 디자인한 지원 위젯까지 오염시킬 것입니다. 호스트 레벨에서 구체적인 값으로 자신만의 타이포그래피, 간격, 텍스트 정렬을 설정하십시오. 부모 페이지가 적대적이라고 가정하고, 당신에게 중요한 모든 것을 초기화하십시오. Shadow DOM은 클래스를 보호할 뿐, 미학(aesthetics)을 보호해주지는 않습니다.
빈 div의 소멸 마술
이 문제는 우리의 허를 완전히 찔렀습니다. Shopify Dawn을 포함한 많은 인기 테마에는 div:empty { display: none; }이라는 무해해 보이는 CSS 규칙이 포함되어 있습니다. 위젯이 마운트될 때, 보통 비어 있는 상태로 시작하는 호스트 div를 대상으로 합니다. JavaScript가 실행되어 React가 노드를 하이드레이션(hydrate)하기 전까지, 그 div는 말 그대로 비어 있습니다. 테마의 스타일시트가 이를 숨겨버립니다. 스크립트는 실행되고 ReactDOM.createRoot를 호출하지만, 아무것도 나타나지 않습니다. 콘솔에는 에러가 없습니다. 요소가 레이아웃에서 단순히 사라져 버린 것입니다. 해결책은 무식할 정도로 명시적인 것입니다. 마운트 포인트에 display: block !important 인라인 스타일을 적용하십시오. 나중에 CSS-in-JS 라이브러리가 이를 처리해 줄 것이라고 기대하지 마십시오. 스타일시트가 적용될 때쯤이면 이미 호스트 테마가 승리한 뒤입니다.
rem 대신 px를 사용하십시오
일반적인 애플리케이션에서는 rem과 같은 상대 단위가 책임감 있는 선택입니다. 하지만 임베드 환경에서 이는 위험 요소입니다. rem 값은 위젯이 아닌 호스트 문서의 루트 html 글꼴 크기를 기준으로 계산됩니다. 만약 호스트 페이지가 html { font-size: 10px; }로 설정하거나 예전의 62.5% 트릭을 사용한다면, 당신의 타이포그래피와 간격 스케일 전체가 예고 없이 틀어집니다. 편안했던 1.6rem의 줄 높이가 16px로 줄어들거나, 패딩이 읽을 수 없을 정도로 얇아질 수도 있습니다. 호스트의 루트 크기를 예측하거나 제어할 수 없기 때문에, 임베디드 위젯에는 픽셀(px)만이 유일하게 정직한 단위입니다. 픽셀은 주변 페이지의 설정과 관계없이 동일한 물리적 크기로 렌더링됩니다. 다른 사이트의 캐스케이드 안에서 살아간다면, rem의 이론적인 접근성 유연성을 포기하고 px의 실질적인 신뢰성을 택하십시오.
설정이 사라지기 전에 읽으십시오
스크립트 태그의 data attributes를 통해 위젯에 설정을 전달하는 경우, 이를 반드시 동기적으로 읽어야 합니다. 브라우저는 스크립트가 자신의 태그를 검사할 수 있도록 document.currentScript를 제공하지만, 이 참조는 일시적입니다. DOMContentLoaded나 다른 비동기 경계(asynchronous boundary)를 기다리면 document.currentScript는 null이 됩니다. 설정값이 증발해 버리는 것이죠. 스크립트 실행 최상위 레벨에서 즉시 해당 속성들을 읽으십시오. API 키, 위젯 ID, 컬러 테마를 그 즉시 캡처하여 클로저(closure)나 모듈 변수에 저장한 다음, 그 후에 React 부팅을 진행하십시오.
스크립트 URL이 API Origin을 결정하게 하세요
번들에 프로덕션 API URL을 하드코딩하는 것은 여러 환경에서 문제가 증폭되는 실수입니다. 대신, 스크립트 요소 자체의 src 속성에서 API origin을 유도하십시오. 위젯이 https://cdn.staging.example.com/widget.js에서 로드된다면, API 호출은 기본적으로 https://api.staging.example.com을 향해야 합니다. 개발자가 localhost:3000에서 서빙되는 로컬 HTML 파일에 스크립트 태그를 넣는다면, 로컬 빌드는 요청을 로컬 서버로 라우팅해야 합니다. 이러한 관례를 따르면 환경별 빌드, 피처 플래그(feature flags), 또는 임베드 사용자의 수동 설정이 필요 없습니다. 인프라 위치가 전달 위치에 의해 암시되므로 그냥 알아서 작동합니다.
캐시 헤더를 핫픽스 생명선처럼 다루세요
사용자들은 푸터 템플릿에 스크립트 태그를 한 번 복사해 넣고는 잊어버립니다. 5,000명의 가맹점에게 이메일을 보내 버전 쿼리 파라미터를 업데이트해 달라고 요청할 수는 없습니다. 이는 캐시 헤더가 여러분의 장애 대응 전략(incident response strategy)의 일부임을 의미합니다. 위젯 번들에 짧은 max-age를 설정하여, 중요한 수정 사항을 배포했을 때 몇 주가 아닌 몇 시간 내에 전파되도록 하십시오. 오래 지속되는 캐시된 자산의 편리함은, 여러분이 회수할 수 없는 고장 난 버전을 수천 개의 사이트가 실행하고 있다는 사실을 알았을 때의 무력감보다 가치 있지 않습니다. CDN 트래픽 비용을 감수하십시오. 여러분의 정신 건강이 거기에 달려 있습니다.
iframe 임베드를 위해 CSP를 뒤집으세요
iframe 기반 임베드 옵션을 제공한다면, 콘텐츠 보안 정책(Content Security Policy)은 일반적인 웹 애플리케이션 사고방식과는 반대로 적용되어야 합니다. 보통은 클릭재킹(clickjacking)을 방지하기 위해 프레이밍을 금지할 것입니다. 하지만 위젯의 경우, 이를 허용해야 합니다. 어떤 사이트든 여러분의 iframe을 호스팅할 수 있도록 frame-ancestors *를 설정하십시오. 그런 다음 그 외의 모든 것에 대해서는 엄격해져야 합니다. 해당 iframe 정책 내에서 script-src, style-src, connect-src를 단단히 잠그십시오. 프레이밍 벡터를 통해 의도적으로 웹 전체에 자신을 노출하는 것이므로, 호스트 페이지가 조작을 시도하더라도 iframe 내부에서 실행되는 코드가 오작동할 여지가 없도록 보장해야 합니다.
게스트 마인드셋
임베드를 구축하는 것은 표준 웹 애플리케이션을 구축하는 것과는 다른 자세를 요구합니다. 자체 앱에서는 컨테이너, 라우팅, 빌드 파이프라인, 글로벌 스타일을 직접 관리합니다. 하지만 임베드에서는 아무것도 소유하지 않습니다. 호스트 페이지는 임의적이고, 종종 오래되었으며, 때로는 적대적이고, 항상 여러분의 통제 밖에 있습니다. 모든 가정은 방어적이어야 합니다. 의도를 명시적으로 지정하고, 환경을 적극적으로 검증하며, 눈에 보이지 않는 파손에 대비해 설계하십시오. Clanker Support 위젯이 오늘날 작동하는 이유는 웹이 예측 가능해서가 아니라, 우리가 웹이 예측 가능할 것이라는 믿음을 버렸기 때문입니다.
