Playwright 로 크로스탭 동기화를 테스트할 수 없었던 이유 — browser.newPage()
browser.newPage() 는 호출할 때마다 새 BrowserContext 를 만든다. 같은 오리진이어도 localStorage 와 BroadcastChannel 이 전혀 공유되지 않는다.
한 탭에서 상태를 바꾸면 다른 탭이 따라오는 기능을 만들었다. localStorage 쓰기 + storage 이벤트 + BroadcastChannel 조합이다.
Playwright 로 검증하려는데 전혀 동작하지 않았다. 조작하는 쪽 탭에서 아무리 바꿔도 받는 쪽 탭 화면이 갱신되지 않는다.
const op = await browser.newPage();
await op.goto('http://localhost:5184/?room=x#operator');
const page = await browser.newPage();
await page.goto('http://localhost:5184/?room=x#display');
// op 에서 클릭해도 page 의 localStorage.getItem(key) 는 계속 null
코드를 몇 번 다시 읽었다. 로직에는 문제가 없어 보였고, 실제로 없었다.
원인 — newPage() 는 context 까지 새로 만든다
Browser.newPage() 는 매 호출마다 새 BrowserContext 를 암묵적으로 생성하는 shorthand 다. 공식 문서에도 이렇게 적혀 있다.
In-Browser: creates a new Page in a new context
별도 BrowserContext 는 별도 프로필이다. 쿠키, localStorage, sessionStorage 가 전부 격리된다.
같은 오리진이어도 진짜 크롬의 “다른 탭”이 아니다. 사실상 다른 사람의 브라우저다. 그러니 오리진 단위로 공유되는 메커니즘인 localStorage 나 BroadcastChannel 이 통할 리가 없다.
일반적인 E2E 테스트에서는 이 동작이 오히려 편하다. 테스트마다 깨끗한 상태로 시작하고 로그인 상태가 안 섞인다. 그래서 이 shorthand 가 기본으로 제공된다. 크로스탭 기능을 테스트할 때만 정확히 반대로 작동한다.
확인은 3초면 된다
같은 key 를 양쪽에서 직접 찍어본다.
console.log(await op.evaluate(() => localStorage.getItem('room:x')));
console.log(await page.evaluate(() => localStorage.getItem('room:x')));
한쪽만 값이 있고 다른 쪽이 null 이면 context 분리다. 앱 로직을 의심하기 전에 이걸 먼저 찍으면 시간을 아낀다.
해결 — context 를 하나 잡고 그 안에서 연다
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
const op = await context.newPage();
const page = await context.newPage();
// 이제 둘 다 같은 localStorage / BroadcastChannel 을 공유한다
browser.newPage() 가 browser.newContext() + context.newPage() 를 합친 것이므로, 앞부분을 한 번만 하고 뒷부분을 두 번 하면 된다.
이러면 실제 사용자가 탭 두 개를 연 상황과 같아진다. 크로스탭 동기화를 진짜로 검증할 수 있다.
규칙으로 만들어두기
이 함정은 한 번 알면 끝이지만, 잊으면 매번 같은 시간을 쓴다. 그래서 기준을 하나 정해뒀다.
여러 페이지가 상태를 공유해야 하면 browser.newContext() 를 하나 잡고 그 안에서 페이지를 연다.
반대로 격리가 필요하면 — 로그인 사용자 두 명, 권한이 다른 두 계정 — 그때는 browser.newPage() 를 여러 번 부르는 게 오히려 정답이다. 격리가 공짜로 따라온다.
즉 어느 쪽이 옳은 게 아니라 무엇을 테스트하는지에 따라 갈린다. 이 API 는 그 선택을 이름으로는 드러내지 않는다는 게 함정의 전부다.