본문 바로가기
실전 개발 노트/프로젝트·회고

React로 브라우저 OS를 만들었다: PocketDesk OS 창 관리자·파일 시스템 개발기

by 쑥쑥자라나라 2026. 7. 31.
728x90

브라우저 탭 하나를 켰는데 시작 메뉴가 열리고, 창을 옮기고, 파일을 저장하고, 메모장과 그림판까지 실행된다면 어떨까. 이 생각에서 PocketDesk OS를 시작했다. Windows를 그대로 복제하려는 프로젝트가 아니라, 데스크톱 운영체제의 익숙한 사용 흐름을 React 애플리케이션 안에서 어디까지 구현할 수 있는지 직접 시험하는 프로젝트다.

현재 공개한 0.1.0 버전에는 시작 메뉴, 작업 표시줄, 다중 창, 파일 탐색기, 휴지통, 웹 브라우저, 메모장, 그림판, 계산기와 지뢰찾기가 들어 있다. PWA로 설치할 수 있고, 브라우저를 닫았다 다시 열어도 창과 설정, 파일이 남는다. 완성된 제품 회고가 아니라 지금도 개발 중인 프로젝트의 첫 번째 기록이다.

이번 글의 질문
화면을 Windows처럼 꾸미는 일을 넘어, 웹앱이 실제 데스크톱처럼 “작동한다”고 느끼게 만드는 요소는 무엇일까?

처음부터 OS를 만들 생각은 아니었다

처음 목표는 작은 웹 데스크톱이었다. 바탕화면에 아이콘을 놓고 앱 창 몇 개를 띄우면 금방 끝날 것 같았다. 하지만 창 하나가 생기자 최소화와 최대화가 필요했고, 창이 둘이 되자 어느 창이 앞에 있어야 하는지 결정해야 했다. 파일 탐색기를 넣으니 메모장과 그림판이 같은 파일을 열어야 했고, 삭제 기능을 만들자 휴지통과 복원까지 연결됐다.

기능은 독립적으로 늘어나지 않았다. 하나의 동작이 다른 상태를 계속 끌고 왔다. 그래서 PocketDesk OS의 핵심은 앱의 개수보다 창·파일·작업 표시줄이 같은 상태를 공유하는 구조가 됐다.

OS처럼 보이는 것과 작동하는 것은 달랐다

배경화면과 아이콘을 비슷하게 배치하면 첫 화면은 그럴듯해 보인다. 하지만 사용자는 몇 초 안에 창을 움직이고, 작업 표시줄을 누르고, 파일을 더블클릭한다. 이때 예상한 동작과 조금만 다르면 화면이 아무리 정교해도 “운영체제처럼 생긴 웹페이지”로 느껴진다.

보이는 요소실제로 필요한 상태놓치기 쉬운 문제
여러 개의 앱 창위치, 크기, 최소화, 최대화, 활성 창, z-index뒤에 있던 창을 눌러도 앞으로 오지 않음
작업 표시줄실행 중인 앱, 고정 앱, 현재 활성 앱버튼을 눌렀을 때 복원과 최소화가 뒤섞임
파일 탐색기폴더 관계, 파일 형식, 연결 앱, 삭제 상태화면에서는 복사됐지만 새로고침하면 사라짐
잠금 화면부팅, 잠금, 로그인, 해제 전환애니메이션 중 입력되어 화면 상태가 꼬임

가장 먼저 부딪힌 문제: 창 관리

창은 단순한 사각형이 아니었다. 모든 창은 고유 ID와 앱 종류, 좌표, 크기, 최소화 여부, 최대화 여부, 앞뒤 순서를 가진다. 창을 클릭하면 z-index가 올라가고, 작업 표시줄의 활성 창을 다시 누르면 최소화된다. 화면 가장자리로 끌면 스냅 위치를 미리 보여준 뒤 크기와 좌표를 함께 바꾼다.

type WindowInstance = {
  id: string;
  appId: AppId;
  x: number;
  y: number;
  width: number;
  height: number;
  z: number;
  minimized: boolean;
  maximized: boolean;
};

최근에는 닫기와 최소화 동작에 170ms 전환 상태를 추가했다. 상태를 즉시 삭제하면 창이 갑자기 사라지고, 애니메이션만 먼저 넣으면 사용자가 같은 창을 다시 열었을 때 이전 타이머가 새 상태를 지우는 문제가 생긴다. 그래서 창마다 실행 중인 타이머를 관리하고, 복원이나 재실행이 들어오면 기존 전환을 취소하도록 바꿨다.

사소해 보이는 움직임이지만 이런 부분이 누적되어 “웹페이지 위 박스”와 “조작 가능한 데스크톱”의 차이를 만든다. 대신 현재 창과 앱 상태가 한 파일에 너무 많이 모여 있다는 기술 부채도 분명해졌다.

728x90

설정과 파일은 같은 곳에 저장하지 않았다

브라우저를 다시 열었을 때 상태를 복원하려면 저장소가 필요하다. PocketDesk OS에서는 데이터 성격에 따라 저장 위치를 나눴다.

  • localStorage: 테마, 배경화면, 창 위치, 작업 표시줄 고정 앱처럼 작고 즉시 읽어야 하는 설정
  • IndexedDB: 파일, 폴더, 바로가기, 휴지통처럼 항목이 늘어나고 구조가 필요한 데이터

IndexedDB의 entries 저장소는 파일 ID를 키로 사용하고, parentIdkind 인덱스를 둔다. 덕분에 같은 파일 데이터가 바탕화면, 파일 탐색기, 메모장, 그림판에서 연결된다.

const store = database.createObjectStore("entries", {
  keyPath: "id",
});
store.createIndex("parentId", "parentId", { unique: false });
store.createIndex("kind", "kind", { unique: false });

여기서 배운 점은 “저장 버튼이 동작한다”와 “데이터가 일관되게 남는다”가 다르다는 것이다. 파일 복사를 화면 배열에만 반영하면 당장은 성공처럼 보인다. 하지만 IndexedDB 쓰기까지 끝나지 않으면 새로고침 후 사라진다. 그래서 Playwright 테스트도 복사 직후 화면만 확인하지 않고 IndexedDB를 다시 열어 항목이 실제로 저장됐는지 검사한다.

브라우저 안의 브라우저는 생각보다 까다로웠다

PocketDesk 안에는 Microsoft Edge라는 이름의 웹 브라우저 앱도 있다. 주소 입력, 검색 엔진 선택, 즐겨찾기, 방문 기록과 읽기 보기를 구현했다. 웹 보기 자체는 iframe을 사용하지만 모든 사이트가 iframe 안에서 열리는 것은 아니다. 대상 사이트가 X-Frame-Options나 CSP의 frame-ancestors로 삽입을 막으면 애플리케이션 코드만으로 우회할 수 없다.

<iframe
  referrerPolicy="strict-origin-when-cross-origin"
  sandbox="allow-downloads allow-forms allow-modals
           allow-same-origin allow-scripts"
  src={url}
/>

따라서 새 탭 열기와 읽기 보기를 함께 제공하고, iframe이 가능한 페이지와 불가능한 페이지를 구분해야 한다. 이 기능을 만들면서 웹 브라우저는 주소창보다 보안 경계와 실패 경험을 어떻게 설명하느냐가 더 중요하다는 것을 체감했다.

GitHub Pages와 PWA에서 놓치기 쉬운 경로

로컬에서는 앱이 /에서 실행되지만 GitHub Pages에서는 /pocket-desk-os/ 아래에 배포된다. 자산과 서비스 워커 경로를 루트 기준으로 고정하면 로컬 빌드는 성공해도 배포 후 아이콘이나 PWA가 깨진다.

Vite의 base 경로를 환경 변수로 정규화하고, 서비스 워커도 import.meta.env.BASE_URL을 기준으로 등록했다. 별도의 Pages 검사 스크립트는 빌드 결과의 manifest, 서비스 워커, 공유 이미지와 자산 경로를 확인한다. 현재 main 브랜치의 CI와 GitHub Pages 배포는 모두 통과한 상태다.

테스트는 화면이 아니라 작업 흐름을 본다

이 프로젝트의 smoke test는 “버튼이 보인다”에서 끝나지 않는다. 잠금 화면을 해제하고, 앱을 실행하고, 창을 스냅하고, 파일을 복사해 IndexedDB에 남았는지 확인하고, 휴지통에서 복원한다. 지뢰찾기와 계산기처럼 상태가 있는 앱도 실제 흐름을 따라간다.

  1. 사용자가 할 행동을 키보드와 마우스로 재현한다.
  2. 창·작업 표시줄·파일 시스템의 상태가 함께 바뀌는지 확인한다.
  3. GitHub Pages 하위 경로에서도 같은 흐름이 유지되는지 검사한다.

UI가 운영체제처럼 복잡해질수록 컴포넌트 단위 확인만으로는 회귀를 찾기 어렵다. 한 기능을 고쳤는데 다른 앱의 창 순서나 파일 연결이 깨질 수 있기 때문이다.

지금 가장 큰 숙제는 9,000줄짜리 App.tsx다

기능을 빠르게 연결하는 동안 App.tsx는 약 9,600줄, 스타일 파일은 약 6,000줄까지 커졌다. 프로토타입 단계에서는 한 파일 안에서 전체 흐름을 추적하기 쉬웠지만, 이제는 수정 범위가 넓어지고 앱별 테스트도 분리하기 어려워졌다.

다음 개발 단계에서는 창 관리자, 가상 파일 시스템, 작업 표시줄, 기본 앱을 모듈로 나누고 상태 경계를 정리할 예정이다. 겉모양을 더 만드는 것보다 지금 있는 기능을 안전하게 분리하는 일이 더 중요해졌다.

다음 개발 기록 후보
① 거대한 App.tsx를 기능별로 분리하는 과정
② IndexedDB 파일 시스템을 안전하게 리팩터링하는 방법
③ iframe이 막힌 사이트를 처리하는 브라우저 UX
④ PWA 오프라인 동작과 업데이트 전략

첫 번째 기록을 마치며

PocketDesk OS를 만들며 가장 크게 배운 것은 데스크톱 UI의 본질이 모양보다 연결에 있다는 점이다. 창, 작업 표시줄, 파일, 앱이 각자 그럴듯하게 보여도 상태가 이어지지 않으면 데스크톱처럼 느껴지지 않는다.

아직 완성 단계는 아니다. 오히려 지금부터 구조를 나누고 실패 경험을 다듬는 일이 시작이다. 다음 글에서는 기능을 더했다는 나열보다, 실제로 막힌 문제와 선택한 해결 방법, 바꾸고 나서 확인한 결과를 계속 기록하려 한다.

728x90