우리는 왜 금융사 웹사이트 안에 SDK를 심었을까

약정 화면을 남의 도메인에 그리기로 했다

책임은 우리에게, 통제권은 금융사에
인앱약정 SDK 시리즈 1편. 대출 비교 플랫폼에서 금융사 도메인 위에 약정 화면을 제공하는 웹 SDK를 만들면서 겪은 이야기다. 15개 금융사와 연동하는 과정에서 마주친 문제들은 대부분 하나의 질문으로 돌아왔다.
“남의 웹사이트 안에서 우리 서비스의 품질을 어떻게 보장할 것인가.”
유저는 어디서 사라졌나
대출 비교 플랫폼의 핵심 경험은 단순하다.
사용자가 여러 금융사의 조건을 비교하고, 원하는 상품을 선택한 뒤 신청까지 이어가는 것.
문제는 신청 이후였다.
약정 단계는 금융사가 담당해야 했다. 플랫폼 안에서 약정까지 끝낼 수 없었기 때문에, 사용자는 신청 버튼을 누른 뒤 금융사 앱이나 웹페이지로 이동해야 했다.
여기서 문제가 생겼다.
비교하고, 고민하고, 신청까지 완료한 사용자가 마지막 단계에서 다시 앱을 설치해야 했다. 로그인을 다시 해야 했고, 본인인증도 다시 거쳐야 했다.
우리가 만들어온 경험이 금융사의 경계 앞에서 끊겼다.
신청까지 도달한 사용자를 마지막 순간에 놓치는 것이 이 프로덕트에서 가장 먼저 해결해야 할 문제였다.
flowchart LR
subgraph BEFORE["기존 여정"]
A1[조건 비교] --> A2[대출 신청]
A2 --> A3["금융사 앱 이동<br/>설치 · 로그인 · 인증"]
A3 --> A4[약정 완료]
A3 -.-> X[이탈]
end
subgraph AFTER["인앱약정"]
B1[조건 비교] --> B2[대출 신청]
B2 --> B3["앱 안에서<br/>약정 완료"]
end
그래서 남의 도메인 안에 들어가기로 했다
처음 요구사항은 모순처럼 보였다.
- 약정 화면은 금융사가 제공해야 한다.
- 하지만 사용자는 우리 서비스를 떠나면 안 된다.
둘 중 하나를 포기해야 할 것 같았다.
해결책은 SDK였다.
구조는 이렇게 됐다.
우리 앱의 웹뷰 안에서 금융사 도메인의 페이지를 열고, 그 페이지 안에 우리가 만든 SDK를 삽입한다.
금융사는 자기 페이지에 SDK 스크립트를 설치한다.
<script src="/static/loan-sdk.js"></script>
<script>
LoanSDK.renderPage({
page: 'agreement',
props: {
onComplete: () => {
// 금융사 처리
}
}
});
</script>금융사 개발자가 호출하는 인터페이스는 단순하다.
renderPage 하나.
하지만 이 한 줄 뒤에는 꽤 복잡한 문제가 숨어 있었다.
왜냐하면 이 화면은 우리 서비스이지만, 실행되는 장소는 우리 서비스가 아니었기 때문이다.
가장 이상한 프론트엔드 환경
일반적인 웹서비스라면 이런 문제를 고민하지 않는다.
우리는 HTML을 만들고,
CSS를 관리하고,
JavaScript 실행 순서를 결정한다.
하지만 SDK는 달랐다.
우리가 만든 화면은 금융사 페이지 안에서 실행된다.
그 페이지의 주인은 금융사다.
금융사는 자기 CSS를 가지고 있고,
자기 JavaScript를 가지고 있고,
자기 배포 프로세스를 가지고 있다.
그런데 사용자가 보는 결과에 대한 책임은 우리에게 있었다.
화면이 깨지면 사용자는 금융사가 아니라 우리 서비스를 탓한다.
에러가 나도,
로딩이 늦어도,
뒤로가기가 이상해도,
사용자는 모두 “우리 서비스가 이상하다”고 생각한다.
하지만 원인을 찾으려고 보면 문제가 발생한 환경은 우리가 통제할 수 없는 곳이다.
이 구조의 핵심 문제는 이것이었다.
책임은 우리에게 있는데, 실행 환경의 통제권은 금융사에 있다.
이 구조에서 마주친 질문들
처음에는 단순한 SDK라고 생각했다.
스크립트 하나 넣고,
함수 하나 호출하면 끝날 거라고.
하지만 금융사 도메인 위에서 우리 서비스를 운영하려면 화면 렌더링 이상의 문제를 풀어야 했다.
| 질문 | 실제 고민 | 편 |
|---|---|---|
| 무엇을 만들지부터 어떻게 합의할 것인가 | 실행 위치, 책임 범위, 호출 규칙이 비어 있는 상태에서 첫 금융사와 무엇을 먼저 확정해야 하는가 | Part 2. 어디에서 실행되나 |
| 어떻게 실행 환경을 격리할 것인가 | 금융사 전역 CSS가 존재하는 페이지 안에서 우리 UI의 품질을 어떻게 보장할 것인가 | Part 3. CSS 격리 |
| 어떻게 SDK를 전달하고 실행할 것인가 | SDK가 안정적으로 로드되고, 호출 시점과 파일명 변경이 장애로 이어지지 않게 하려면 어떻게 해야 하는가 | Part 4. 어떻게 전달하나 |
| 어떻게 여러 금융사를 확장할 것인가 | 금융사마다 다른 요구를 수용하면서도 복사 방식에서 벗어날 수 있는 구조는 무엇인가 | Part 5. 어떻게 확장하나 |
| 누가 다음 화면을 결정할 것인가 | 타입 검사가 없는 환경에서 금융사의 업무 판단과 SDK의 화면 흐름 제어 책임을 어떻게 나눌 것인가 | Part 6. 어떻게 연결하나 |
| 어디까지 관측할 수 있는가 | 금융사가 처리한 예외와 실행되지 않은 구간을 어떻게 바라볼 것인가 | Part 7. 어떻게 관측하나 |
| 사용자가 실제로 무엇을 봤는가 | 로딩과 렌더링 사이의 간극을 어떻게 정의하고 측정할 것인가 | Part 8. 어떻게 기다리게 하나 |
| 누가 실행 상태를 소유하는가 | WebView, history, document 교체 상황에서 화면 흐름과 상태를 어떻게 유지할 것인가 | Part 9. 어떻게 이어가나 |
| 배포한 SDK가 실제로 동작하는가 | 금융사마다 다른 적용 상태와 버전을 어떻게 관리할 것인가 | Part 10. 어떻게 운영하나 |
각각 다른 문제처럼 보였다.
CSS 문제, 로딩 문제, 계약 문제, 화면 흐름 문제, 관측 문제.
하지만 결국 하나의 질문으로 돌아왔다.
남의 실행 환경 안에서, 우리 서비스의 품질을 어디까지 책임질 수 있는가.
이후의 설계는 모두 이 질문에 답하는 과정이었다.
우리가 처음부터 가지고 있던 제약
이런 문제가 있었다면 가장 먼저 떠오르는 해결책이 있다.
“그냥 iframe으로 격리하면 되는 것 아닌가?”
맞다.
기술적으로는 가장 깔끔한 방법이다.
하지만 우리 상황에서는 선택할 수 없었다.
약정 화면은 금융사가 제공하는 형태여야 했고, 서비스 구조상 금융사 도메인 안에서 실행되어야 했다.
결국 우리는 가장 어려운 위치를 선택했다.
완전히 격리된 외부 영역도 아니고,
완전히 통제 가능한 우리 서비스 영역도 아닌,
남의 페이지 안에서 우리 서비스를 운영하는 구조.
이 선택 때문에 이후 모든 아키텍처 결정이 시작됐다.
각 편에서 다룬 문제
여기까지가 시리즈의 출발점이다. 아래는 각 편이 실제로 어떤 문제를 만났고 무엇으로 답했는지를 정리한 것이다.

Part 2. 어디에서 실행되나
화면만 그리면 되는 일이라고 생각했다
첫 금융사 연동에서 가장 오래 걸린 일은 코드를 쓰는 일이 아니라 무엇을 만들지 합의하는 일이었다. 실행 위치와 책임 범위, 호출 규칙을 먼저 확정하고 React SPA 구조와 renderPage 단일 진입점을 정했다.
이때 내린 결론은 SDK가 화면을 그리는 일만 책임지면 된다는 것이었다. 이후의 편들은 그 결론이 어디까지 부족했는지를 하나씩 확인하는 과정이다.
Part 3. CSS 격리
남의 document 안에서 살아남기
금융사 전역 스타일이 우리 버튼과 폰트를 덮었다. 검증할 조합이 금융사 수와 화면 수의 곱으로 늘면서 폐쇄망에 노트북을 들고 들어가 DevTools를 열어야 하는 일이 반복됐다.
덮어쓰기 → cross-origin iframe → friendly iframe 순으로 검토하고 접은 뒤 Shadow DOM을 택했다. 경계가 막아주지 않는 rem 기준값, 폰트 이름, React Portal의 도착지를 각각 따로 처리한 과정까지 다룬다.
Part 4. 어떻게 전달하나
로딩 타이밍과 배포 책임을 SDK 안으로 가져오기
사무실에서는 재현되지 않던 흰 화면 장애의 원인은 SDK가 내려오기 전에 금융사 코드가 먼저 호출한 것이었다. 배포마다 바뀌는 해시 파일명 때문에 우리 수정 하나가 금융사 전체의 배포 일정에 묶이기도 했다.
호출 시점은 커맨드 큐로 먼저 받아 두고, 금융사 HTML은 이름이 고정된 엔트리만 참조하게 바꿔 해시 청크를 우리가 교체했다. 두 문제 모두 통제할 수 없는 곳에 중요한 결정을 맡기고 있었다는 점에서 같은 문제였다.
Part 5. 어떻게 확장하나
30개의 금융사를 지원하기 위한 SDK 표준화 설계
첫 금융사 코드를 복사해 늘리는 방식으로는 금융사 30곳과 화면 25개가 곱해져 750개를 관리해야 했다. 기획서 3건을 나란히 놓고 보니 UI는 전부 달랐지만 업무 기준으로는 80% 이상이 겹쳤다. 30곳은 이 시점의 영업 목표였고, 앞서 말한 15개는 이 표준화 이후 개발이 확장 속도를 따라가면서 실제로 도달한 수다.
화면을 업무 유형·표현 유형·전환 규칙으로 정의하고, 공통에 넣기 어려운 요구는 Custom Page라는 자리로 열어 뒀다. 표준화의 목표는 차이를 없애는 것이 아니라 차이가 들어올 자리를 정해 두는 것이었다.
Part 6. 어떻게 연결하나
다음 화면은 누가 정하는가
화면을 그리는 건 SDK인데 다음 화면을 고르는 건 금융사 코드였다. 이벤트를 아무리 모아도 사용자가 왜 그 화면에 도달했는지 설명할 기준이 없었다.
renderPage('rejection') 한 줄에 섞여 있던 업무 판단과 화면 연결을 분리했다. 금융사는 업무 결과만 보고하고, 화면을 잇는 규칙은 빌드 시점 그래프로 SDK가 소유하게 했다.
Part 7. 어떻게 관측하나
오류의 원인을 볼 수 없는데 화면은 어떻게 책임질 것인가
금융사 콜백 안에서 try/catch로 처리된 예외는 우리 전역 핸들러까지 오지 않는다. console이나 Promise를 패치해 복원하는 방법도 검토했지만, 남의 페이지가 원래대로 실행된다는 예측 가능성을 잃는 대가가 컸다.
목표를 예외를 찾는 일에서 약속이 어디까지 이행됐는지 보는 일로 바꿨다. 전환 하나를 5개 신호로 나누고, timeout은 실패 처리가 아니라 관측 기준으로만 썼다.
Part 8. 어떻게 기다리게 하나
로딩을 띄웠는데 사용자는 보지 못했다
전환 중 흰 화면을 막으려고 로딩을 띄웠는데, 이전 화면과 함께 사라지거나 표시 코드는 실행됐는데도 화면에 나타나지 않는 일이 생겼다. 원인은 소유권·시점·document 경계·종료 조건 네 갈래였다.
전환 중에 남아야 하는 UI는 전환이 파괴하는 수명 경계보다 바깥에서 소유해야 한다. 생김새가 같다는 이유로 디자인시스템 모달을 재사용한 것이 문제의 출발점이었다.
Part 9. 어떻게 이어가나
같은 document인가, 새 document인가
다음 화면을 정하는 것과 그 화면까지 실제로 이동하는 것은 다른 일이었다. 금융사 라우터의 navigate() 안에는 SDK가 끼어들 lifecycle hook이 없었고, WebView의 뒤로가기 입력은 기본적으로 Web에 닿지 않았다.
이동은 navigate() 호출 전에 React root부터 정리하는 순서로 풀었고, 뒤로가기는 목적지를 금융사 콜백에 맡기고 SDK가 그래프로 확인하는 구조로 풀었다. document가 교체되는 MPA 금융사에서도 같은 그래프로 검증할 수 있었다.
Part 10. 어떻게 운영하나
고칠 수 없는 자리는 그대로 두고, 고쳐야 할 일을 줄인다
번들은 하나를 만들지만 실제로 도는 버전은 금융사 수만큼 갈라졌다. 파일을 올리는 마지막 한 걸음이 금융사 배포 일정에 묶여 있었고, 그 드리프트는 장애를 볼 때마다 sdkVersion을 먼저 확인하게 만들었다.
웹뷰가 번들을 직접 주입하는 안은 CSP와 계약 교체 문제로 접었다. 대신 질문을 번들을 갈아끼울 수 있는가에서 갈아끼워야 하는 일을 얼마나 줄일 수 있는가로 바꿨다. 아홉 편의 구조를 한 장으로 다시 조립하고, 아직 풀지 못한 과제를 정리한다.
시리즈 전체를 관통하는 질문
이 시리즈에서 계속 등장하는 질문은 하나다.
“내 코드가 남의 환경에서 실행될 때, 어디까지 통제할 수 있는가?”