같은 사이트를 같은 브라우저에서 열었는데도 저장소를 공유하지 않는 경우가 있다. 그 경계를 정하는 게 브라우저 프로필이다.

업무에서 사용자가 여러 창을 띄워 같은 작업을 동시에 진행하다가 브라우저 스토리지의 데이터가 꼬이는 문제를 다룬 적이 있다. 해결책으로 Web Locks API(navigator.locks)를 검토하면서 “이 락은 정확히 어디까지 공유되지?”를 따라가 봤다. 처음에는 origin만 같으면 되는 줄 알았는데, 파고들수록 프로필, 시크릿 창, 컨테이너 탭 같은 경계가 여러 겹으로 나왔다.

이번 글에서는 브라우저가 저장소를 어떤 단위로 나누는지, 그중 프로필이 정확히 무엇이고 브라우저마다 어떻게 다른지, 그리고 웹 개발자 입장에서 무엇을 신경 써야 하는지 정리했다.


① 저장소 경계는 네 겹이다

브라우저가 탭에 저장소를 나눠 주는 경계는 바깥부터 브라우저 설치 → 프로필 → 브라우징 컨텍스트 → 스토리지 키 순서다. 두 탭은 이 네 가지가 모두 같아야 같은 저장소를 쓴다.

브라우저 설치 Chrome, Edge, Firefox는 서로 완전히 별개 프로필 Default, Profile 1 … 디스크 위의 폴더 브라우징 컨텍스트 일반 창 / 시크릿·게스트 / 컨테이너 탭 스토리지 키 (최상위 사이트, 프레임 origin) 여기까지 같아야 공유 localStorage · IndexedDB · BroadcastChannel · Web Locks

하나라도 다르면 localStorage도, Web Locks도 공유되지 않는다. 바깥 층부터 하나씩 살펴보자.


② 프로필의 정체: 디스크 위의 폴더 하나

결론부터 말하면 프로필은 디스크에 있는 폴더 하나다. 그 폴더가 쿠키, localStorage, IndexedDB 같은 사이트 데이터를 통째로 들고 있다.

Chromium 문서를 보면 user data 디렉터리에는 방문 기록, 북마크, 쿠키 같은 프로필 데이터와 설치 단위의 로컬 상태가 들어 있고, 각 프로필은 그 아래의 하위 폴더(보통 Default)다. 프로필을 추가하면 Profile 1, Profile 2 같은 폴더가 생긴다.

OS Chrome user data 디렉터리 기본 위치
Windows %LOCALAPPDATA%\Google\Chrome\User Data
macOS ~/Library/Application Support/Google/Chrome
Linux ~/.config/google-chrome

지금 쓰는 탭이 어느 프로필인지는 주소창에 chrome://version을 입력해 “프로필 경로” 항목을 보면 바로 알 수 있다.

구글 계정 로그인과는 별개다

나도 처음엔 “프로필 = 구글 계정”이라고 생각했는데, 그렇지 않았다. 계정에 로그인하지 않아도 Chrome은 처음부터 Default 프로필로 동작한다. Google 도움말도 프로필을 북마크, 방문 기록, 비밀번호, 설정을 분리하는 수단으로 설명하고, 계정 로그인은 그 데이터를 다른 기기와 동기화하는 선택 사항으로 다룬다.

즉 로그인은 프로필 위에 동기화를 얹는 기능일 뿐이고, 저장소가 나뉘는 단위에는 영향을 주지 않는다. 프로필을 삭제하면 그 프로필의 데이터가 컴퓨터에서 지워진다는 점도 “프로필 = 로컬 폴더”라는 걸 보여준다.

브라우저 내부에서는

Chromium 코드에서 프로필 하나는 BrowserContext에 대응한다. 그 안에서 실제 저장소를 관리하는 게 StoragePartition인데, 소스 주석은 이를 “각 자식(렌더러) 프로세스가 BrowserContext 안의 영속 상태를 바라보는 뷰”라고 정의한다.

StoragePartitionImpl은 DOM Storage(localStorage), IndexedDB, Cache Storage, Service Worker, BroadcastChannel 같은 컨텍스트를 함께 들고 있다. 탭은 생성될 때 어느 BrowserContext 소속인지 정해지고, 같은 객체를 바라보는 탭끼리만 이 저장소들을 공유한다.

BrowserContext (프로필)
└── StoragePartition
    ├── DOM Storage (localStorage)
    ├── IndexedDB
    ├── Cache Storage
    ├── Service Worker
    └── BroadcastChannel

같은 user data 디렉터리로 Chrome을 한 번 더 실행하면 새 프로세스가 아니라 이미 떠 있는 브라우저로 요청이 넘겨진다. 반대로 --user-data-dir 플래그로 다른 폴더를 지정하면 완전히 별개의 브라우저 인스턴스가 된다.


③ 시크릿 창과 게스트 모드: 잠깐 쓰고 버리는 컨텍스트

시크릿 창과 게스트 모드는 각각 일반 프로필과 분리된 별도의 저장소를 쓰고, 세션이 끝나면 그 저장소를 통째로 버린다.

시크릿 창

시크릿 창을 열면 Chrome은 별도의 브라우징 세션을 시작하고, 이 세션은 시크릿 창을 전부 닫아야 끝난다. 세션 동안에는 사이트가 동작하도록 쿠키와 사이트 데이터를 임시로 보관하고, 세션이 끝나면 지운다. 시크릿 창을 하나 더 열면 새 세션이 아니라 기존 시크릿 세션이 그대로 이어진다.

웹 개발자 관점에서 정리하면 이렇다.

  • 같은 프로필에서 연 시크릿 창끼리는 localStorage, IndexedDB, Web Locks를 공유한다.
  • 시크릿 창과 일반 창은 서로 공유하지 않는다.
  • 마지막 시크릿 창을 닫는 순간 전부 사라지고, 다음에 여는 시크릿 창은 빈 상태에서 시작한다.

Chromium 내부에서 시크릿 세션은 각 일반 프로필에 딸린 off-the-record 프로필로 구현된다. 그래서 프로필이 두 개면 시크릿 세션도 프로필마다 따로 생긴다.

게스트 모드

게스트 모드는 어느 프로필에도 속하지 않는 독립된 임시 컨텍스트다. 게스트 모드에서는 다른 프로필의 정보를 보거나 바꿀 수 없고, 게스트 창을 닫으면 방문 기록, 쿠키, 사이트 데이터가 삭제된다.

게스트 창 안에서 연 탭끼리는 저장소와 락을 정상적으로 공유한다.

“게스트라서 Web Locks가 안 된다”가 아니라, “게스트 창끼리만 공유된다”가 정확한 표현이다.


④ 브라우저별 차이

“프로필로 저장소를 나눈다”는 큰 틀은 같지만, 그 안에서 추가로 나누는 장치와 프라이빗 모드의 동작은 브라우저마다 다르다. 특히 Safari 개인정보 보호 브라우징은 탭끼리도 저장소를 공유하지 않는다.

브라우저 프로필 단위 프라이빗 모드 그 밖의 분리 장치
Chrome user data 디렉터리 아래 폴더 하나 같은 프로필의 시크릿 창끼리 공유, 모두 닫으면 삭제 게스트 모드
Edge Chrome과 동일한 Chromium 구조 InPrivate, 시크릿과 같은 개념 자동 프로필 전환, IE 모드, WebView2
Firefox profiles.ini에 등록된 프로필 폴더 사생활 보호 창, 저장소 키에 표시가 붙어 분리 컨테이너 탭
Safari Safari 17부터 프로필 지원 개인정보 보호 브라우징, 탭마다 격리 없음

Edge

Edge는 Chromium 기반이라 프로필, InPrivate, 게스트의 구조가 Chrome과 같다. 디스크 위치만 %LOCALAPPDATA%\Microsoft\Edge\User Data로 다르다. 그래서 같은 PC에서 Chrome과 Edge로 같은 사이트를 열어도 두 탭은 서로를 전혀 모른다.

Edge에서 특히 눈여겨볼 기능은 자동 프로필 전환(Automatic Profile Switching)이다. 개인 프로필에서 회사·학교 계정으로 인증하는 사이트에 들어가면, Edge가 이를 감지해 회사 프로필로 전환하라고 안내하고 선택한 프로필에서 사이트를 연다. 외부 앱에서 여는 링크를 어느 프로필로 열지 기본값도 정할 수 있다.

사용자 입장에서는 “같은 Edge”지만 실제로는 다른 프로필이라 저장소가 공유되지 않는 상황이 생길 수 있다. 사내 업무용 서비스라면 충분히 만날 수 있는 경우라서 기억해 둘 만하다.

그 밖에도 알아둘 것이 두 가지 있다.

  • IE 모드 탭은 Chromium이 아니라 예전 IE 엔진으로 렌더링되고 저장소도 별개라서, navigator.locks 같은 최신 API 자체가 없다.
  • Edge 엔진을 앱에 임베드하는 WebView2는 user data 폴더(UDF)에 쿠키, 권한, 캐시 같은 데이터를 저장한다. 하나의 UDF 아래 여러 프로필을 둘 수 있고, 프로필마다 전용 폴더가 생겨 데이터가 분리된다. 결국 Edge 브라우저와 WebView2 앱, 그리고 WebView2 앱끼리도 기본적으로 저장소를 공유하지 않는다.

Firefox

Firefox도 가장 바깥 단위는 프로필 폴더다. 흥미로운 건 그 안에서 저장소를 나누는 방식이다.

Firefox는 origin에 OriginAttributes라는 꼬리표를 붙여, origin이 같아도 다른 보안 컨텍스트로 구분한다. 여기에는 이런 값들이 들어간다.

  • userContextId: 컨테이너 탭
  • privateBrowsingId: 사생활 보호 모드 여부
  • partitionKey: 스토리지 파티셔닝

컨테이너 탭은 이 구조를 사용자에게 드러낸 기능이다. 각 컨테이너 탭은 쿠키와 localStorage 같은 저장소를 다른 컨테이너와 따로 쓴다. 즉 Firefox에서는 같은 프로필, 같은 창 안에서도 컨테이너가 다르면 저장소가 분리된다.

Safari

Safari는 17부터 프로필을 지원한다. 프로필마다 방문 기록, 쿠키, 웹사이트 데이터, 확장 프로그램, 탭 그룹, 즐겨찾기가 따로 있고, 처음 프로필을 만들면 기존 데이터는 “개인”이라는 기본 프로필로 들어간다.

정리하면서 가장 놀랐던 부분은 개인정보 보호 브라우징이었다. Apple 도움말은 개인정보 보호 브라우징 창의 각 탭이 서로 격리되어 있다고 설명한다. Chrome 시크릿 창에서는 탭끼리 저장소를 공유하지만, Safari 개인정보 보호 브라우징에서는 같은 사이트를 탭 두 개로 열어도 서로 다른 저장소를 쓴다. 이 경우 Web Locks로 탭끼리 조율하려는 시도는 애초에 성립하지 않는다.


⑤ 프로필 안에서 한 번 더: 스토리지 파티셔닝

같은 프로필 안에서도 저장소는 origin 하나가 아니라 (최상위 사이트, 프레임 origin) 조합으로 다시 나뉜다. 이걸 스토리지 파티셔닝이라고 부른다.

파티셔닝 이전에는 서드파티 iframe이 어느 사이트에 임베드되든 같은 저장소를 써서, 여러 사이트를 넘나드는 추적에 악용될 수 있었다. 지금 Chrome에서는 데이터가 origin도 같고 최상위 사이트도 같은 컨텍스트에서만 보인다.

https://app.example.com 을 탭에서 직접 열면
  → 키: (example.com, app.example.com)

같은 페이지가 https://partner.com 안의 iframe으로 열리면
  → 키: (partner.com, app.example.com)   ← 위의 탭과 저장소·락 공유 안 함

A 사이트 안에 B iframe이 있고, 그 안에 다시 A iframe이 있는 경우도 있다. 최상위와 현재 프레임만 보면 안쪽 A가 1st-party처럼 보이기 때문에, Chrome은 중간에 cross-site 문서가 끼어 있으면 켜지는 “ancestor bit”를 키에 추가로 넣는다.

파티셔닝 대상에는 Web Storage, IndexedDB, Cache Storage, BroadcastChannel, SharedWorker, Web Locks 등이 포함된다. 다만 대부분의 서비스처럼 내 사이트를 최상위 탭으로 여는 경우에는 키가 언제나 같으니, 실무에서 이 층이 문제가 되는 건 다른 사이트에 iframe으로 임베드될 때뿐이다.


⑥ 웹 개발자에게 의미하는 것

탭끼리 무언가를 공유하는 브라우저 API는 전부 저장소 경계를 그대로 따른다. 그래서 “이 API는 어디까지 닿는가”는 “이 탭들이 같은 저장소를 쓰는가”와 같은 질문이다.

Web Locks는 저장소 버킷 단위다

Web Locks 명세는 락 조율이 같은 저장소 버킷을 공유하는 에이전트(탭, iframe, 워커) 사이에서 이루어진다고 정의한다. 각 저장소 버킷은 락 매니저를 하나씩 가지고, 같은 버킷을 쓰는 페이지와 워커는 서로 무관한 창이어도 락 매니저를 공유한다.

이 정의 덕분에 앞에서 본 모든 경계가 락에도 그대로 적용된다. 아래 경우는 모두 락을 공유하지 않는다.

  • 다른 프로필
  • 시크릿·InPrivate 창, 게스트 창
  • Firefox의 다른 컨테이너
  • Safari 개인정보 보호 브라우징의 다른 탭
  • cross-site iframe

localStorage, IndexedDB, BroadcastChannel도 마찬가지다.

경계가 같아서 오히려 안전한 경우

락이 닿지 않는 곳에는 보통 보호할 공유 자원도 없다. 예를 들어 여러 탭에서 토큰 갱신이 동시에 일어나는 걸 Web Locks로 막는다고 해보자.

게스트 창의 탭들은 게스트 창의 토큰을 같이 쓰고 락도 그 탭들끼리 공유하니, 딱 보호해야 할 범위를 보호한다. 일반 창의 탭은 애초에 토큰이 따로라서 게스트 탭과 경쟁할 일이 없다. 저장소 경계와 락의 경계가 같기 때문에 오히려 자연스럽게 맞아떨어지는 셈이다.

경계 밖의 동시성은 서버 몫이다

문제는 보호하려는 자원이 브라우저 밖에 있을 때다. “같은 작업 건은 한 화면에서만 편집해야 한다” 같은 요구사항이라면, 같은 계정으로 다른 프로필이나 다른 브라우저, 다른 PC에서 들어오는 경우를 프론트에서는 막을 수 없다.

이런 경우는 서버에서 버전 체크(낙관적 락)나 상태 체크로 막아야 한다. 이번에 가장 크게 느낀 부분도 이거였다. 프론트에서 할 수 있는 조율에는 분명한 경계가 있다.

JS로 “같은 저장소인가” 확인하기

웹 페이지가 “지금 몇 번 프로필이다”를 알아낼 방법은 없다. 프라이버시 때문에 의도적으로 노출하지 않는다. 대신 실제로 공유되는지는 직접 확인할 수 있다.

// 창 A: 락을 잡고 놓지 않는다
navigator.locks.request('probe', () => new Promise(() => {}));

// 창 B: 같은 저장소라면 held 목록에 'probe'가 보인다
const { held } = await navigator.locks.query();
console.log(held.some((l) => l.name === 'probe'));

BroadcastChannel로 메시지를 보내서 다른 창이 받는지 보는 것도 같은 원리다. 디버깅할 때 “왜 이 두 창은 락을 공유하지 않지?” 싶으면 이 방법으로 먼저 같은 저장소인지부터 확인하면 된다.


정리

“같은 브라우저에서 열었으니 저장소도 같겠지”라는 가정은 생각보다 자주 틀린다.

경계 이게 다르면 저장소가 갈린다
브라우저 설치 Chrome ↔ Edge, --user-data-dir로 띄운 별도 인스턴스
프로필 Default ↔ Profile 1, Edge 자동 프로필 전환
브라우징 컨텍스트 일반 ↔ 시크릿·게스트, Firefox 컨테이너, Safari 개인정보 보호 탭
스토리지 키 직접 연 탭 ↔ 다른 사이트에 임베드된 iframe

브라우저 API로 조율할 수 있는 건 같은 저장소를 쓰는 탭까지이고, 그 밖의 동시성은 서버가 책임져야 한다. 그리고 디버깅할 때는 “두 창이 정말 같은 저장소를 쓰고 있는가”부터 확인하는 게 빠르다.


참고 자료