[Part 2.] 웹브라우저 멀티에디터 만들기 - 아키텍쳐 설계 변화

2026. 3. 26. 00:42개발로그

← 이전 글: [Part 1] 웹브라우저 멀티에디터 만들기 — 아키텍처 설계 https://insight74278.tistory.com/19

 

이전 글 요약

이전 글 [Part 1.] 웹브라우저 멀티에디터 만들기 - 아키텍쳐 설계(https://insight74278.tistory.com/19) 에서는 레이어드 아키텍쳐 + 단방향 데이터 흐름에 기반한 6계층의 수평 레이어(Page > Component > Hook > Facade > Store > Service)와 비즈니스 모델 기반 수직 분할(Video, Audio, Text, Image, Timeline, Project)을 통합한 격자형 아키텍쳐를 설계하였다. 

 

이후 타임라인 기능을 본격적으로 구현하면서, 설계를 다시 한번 크게 수정하게 되었다. 이 글은 그 과정과 결과를 정리한 것이다. 

 

1. 우선 짚고 넘어가고 싶은 것 

1-1. Facade 는 레이어가 아니다. 

이전 글에서 혼란의 여지가 있게 설명한것 같아서 정정하고싶다. Facade를 하나의 독립된 레이어처럼 표현하였는데, Facade는 레이어가 아니라 패턴이다. 레이어란 시스템 전반에 걸쳐 동일한 추상 수준을 갖는 수평적 경계이다. Facade는 여러 Store를 조합하는 비즈니스 로직의 구현 방식이지, 그 자체가 하나의 계층을 형성하는것은 아니다.

 

그렇다면 어디에 위치하는것이 적절한가? 이는 선택의 문제지만, 보통 비즈니스 로직 레이어 안에서 사용되는것이 자연스럽다. 클래스 형태든 함수 형태든, 비즈니스 로직을 작성할 때 Facade 패턴을 활용하는것이다. (Facade 패턴활용에 대한 글은 추후 따로 다루도록 하겠다.)

 

파사드 패턴 ( façade 라고도 표기 )은 객체 지향 프로그래밍 에서 흔히 사용되는 소프트웨어 디자인 패턴 입니다 . 건축의 파사드(façade )와 유사하게, 더 복잡한 내부 구조 코드를 숨기는 전면 인터페이스 역할을 하는 객체 입니다 . 

출처: https://en.wikipedia.org/wiki/Facade_pattern

 

1-2. 아키텍처 설계 단계에서 꼭 수직분할을 생각해야할까?

다시 처음으로 돌아와서, 아키텍처의 설계목적은 변경의 영향 범위를 제한하는 것 뿐만아니라, 이 구조도를 함께 작업을 하는 팀원들에게 설명할 수 있어야 한다. 막상 작업해보니, 수직적으로 도메인간 의존이 얽히는 경우는 생각보다 많지 않았을뿐더러, 수평이 아닌 수직 분할까지 고려한 아키텍처를 공유하려니 불필요한 정보가 너무 많이 섞여 중요하게 지켜져야하는 수평 계층의 분할까지 잘 전달되지 않는 상황이 벌어졌다. 

 

또 하나, 수직분할은 코드 작성 규칙 단에서 정리할 수 있었다. 단순한 규칙이다. 

도메인 간 직접 import 금지, 도메인 간 통신은 반드시 공유 Store를 통해서만 한다.

 

이렇게 규칙을 지정해놓으면 굳이 아키텍쳐 다이어그램에 수직 분할을 포함하지않아도, 충분히 지켜질 것이라 예상된다. 따라서 이번에는 수평분할만을 아키텍쳐로 설계하고, 수직 분할은 단순한 도메인 폴더 분리 + 의존성 규칙으로 충분하다고 판단했다. 

 

2. Timeline 정보를 관리하는 또 하나의 수평 레이어의 필요성

이전 글에서는 Timeline을 그저 video, image 등과 같은 또 하나의 도메인으로만 생각해, 수직분할로 구분하였다. 하지만 실제적인 작업에 들어가보니, Timeline 레이어의 특성은 매우 달랐다. 

 

타임라인은 모든 요소의 정보를 알고 있어야 한다. 

 

비디오, 오디오의 현재 재생시간, 이미지, 텍스트의 시작과 끝나는 시간 등 모든 요소의 시간 정보를 타임라인이 조합하여 하나의 통합된 편집본 형태로 그려야 한다. 재생 헤드가 움직이면 모든 미디어 타입이 동기화되어야하고, 세그먼트를 자르면 해당 미디어의 상태도 함께 변해야 한다. 

Video 도메인 — 썸네일, 비디오 디코딩, 필터         ← 자기 자신만 알면 된다
Audio 도메인 — 파형, 볼륨, 페이드                   ← 자기 자신만 알면 된다
Text 도메인  — 폰트, 스타일, 인라인 편집            ← 자기 자신만 알면 된다
Image 도메인 — ImageBitmap, 이미지 디코딩           ← 자기 자신만 알면 된다

Timeline     — 위의 모든 도메인의 시간축 정보를 알아야 한다  ← 전혀 다른 성격

 

다른 도메인들은 자기 자신만의 고유 로직을 갖고 있으면 되지만, Timeline은 모든 도메인을 횡단하여 조율하는 역할을 한다. 즉,

 

Timeline 의 관심사는 수직적이 아니라 수평적이다. 

 

기존 아키텍쳐에는 이 문제를 해결할 적절한 계층이 없었다. Store는 순수한 상태 저장소이고, 비즈니스 로직은 각 도메인 안에 격리되어 있다. 그런데 타임라인은 도메인을 횡단하는 세션 수준의 상태관리가 필요했다. 이러한 이유로, 수평 레이어를 따로 분리해야겠다는 결론에 도달했다.

 

그렇다면 타임라인 외에도 횡단적 관심사를 가져야하는 기능들은 어떤 것들이 있을까? 기능을 붙이면서 추가되겠지만, 지금 당장 생각해보면 다음 목록 정도다. 

Play Session: 현재 재생 시간을 기준으로 모든 미디어 상태를 동기화
Select Session: 현재 선택된 리전/트랙 정보를 여러 도메인들이 공유
History Session: Undo/Redo 시 모든 도메인의 스냅샷을 관리
Export Session: 모든 미디어를 합성하여 최종 결과물을 생성 

 

이것들은 특정 도메인에 속하지 않으면서, Store보다는 상위의 조율(orchestration) 역할을 한다. 이 계층을 Session Layer라고 이름 붙였다. 

OSI 5계층인 세션 계층(Session Layer)은 통신을 수행하는 두 시스템 간의 연결을 설정하고 유지하며 종료하는 역할을 담당한다. 데이터 자체를 전송하지는 않지만, 통신 흐름이 끊기지 않도록 관리하는 중간 관리자 역할을 수행한다.

출처: https://d-steady.tistory.com/entry/OSI-5%EA%B3%84%EC%B8%B5Session-Layer-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%E2%80%93-%EC%84%B8%EC%85%98-%EA%B3%84%EC%B8%B5%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%84-%EA%B4%80%EB%A6%AC%ED%95%A0%EA%B9%8C

https://en.wikipedia.org/wiki/Session_layer

 

예전에 Drop.daw (내가 만든 웹브라우저용 DAW 엔진 라이브러리 | https://github.com/whizzkid1452/drop.ai)를 만들때, Ardour라는 DAW오픈소스(https://github.com/ardour)를 열어본적이 있는데, 이곳에서도 Session Layer 라는 계층을 따로 두고 있었다. 이러한 구조를 본 적이 있기때문에 무의식적으로 "그럼 이 계층을 Session Layer 로 두면 되겠다! " 하고 생각했는데, 이번에 글을 적으며 Session Layer 라는 단어에 대해 검색해보니 OSI 5계층을 뜻하는 단어였다. (Ardour 에서는 아마 이러한 개념을 차용하여 이름붙인거겠지?) 통신을 수행하는 두 시스템 간의 연결을 설정하고 유지하며 종료하는 중간관리자역할을 담당한다고 하니, 위에서 언급한 기능들을 모아둔 레이어로 딱 알맞는 이름이라는 생각이 들었다. 위 개념의 출처에서 언급한 dd_blog 에서 이 개념을 상세히 잘 다루고 있으니 궁금하신 분들은 참고하시면 좋을 것 같다.  dd_blog

 

3. 프랙탈 구조, 과연 이 프로젝트에 적절할까?

아직 끝나지 않았다. 아키텍쳐는 한번 정한다고 끝이 아니라 기능이 붙을때마다 수정이 필요한게 당연한가보다. 프랙탈 구조는 아키텍쳐라기보다는 폴더구조에 더 가깝지만, 이부분도 내가 생각하지 못했던 부분이라 언급하고싶었다. 나는 여태껏 일반적인 디자인 중심의 프로젝트에서 항상 프랙탈 구조를 사용해왔다. (다음내용부터 나오는 프랙탈 구조에 대한 설명은 이 블로그를 참고했다. 프랙탈 구조에 대해 설명이 잘 되어있는 좋은 글이니 궁금하신분들은 살펴보시면 좋을 것 같다. https://velog.io/@heeflee_1310/%ED%94%84%EB%9E%99%ED%83%88FSD-%ED%8C%8C%EC%9D%BC-%EA%B5%AC%EC%A1%B0)

 

프랙탈(영어: fractal) 또는 프랙털은 일부 작은 조각이 전체와 비슷한 기하학적 형태를 말한다. 이런 특징을 자기 유사성이라고 하며, 다시 말해 자기 유사성을 갖는 기하학적 구조를 프랙탈 구조라고 한다.

출처: https://ko.wikipedia.org/wiki/%ED%94%84%EB%9E%99%ED%83%88

 

쉽게말해, 프랙탈이란 작은 구조가 전체 구조와 닮은 형태로 끝없이 되풀이되는 구조이다. 간단히 설명하자면 개발에서 흔히 쓰이는 프랙탈 구조라는 말은, 폴더 구조에서 각 폴더가 동일한 내부구조를 반복하는 구조이다. 이로써는 흔히 다음과 같은 장점들이 있다. 

 

1. 직관적이다. 
2. 로컬 응집성이 높다. 
3. 모듈단위 독립성이 좋다. 
4. 구조 자체가 설명적이다. 

 

 

위와 같은 이유로 기존까지는 프랙탈구조를 사용해왔었다. 하지만 현재 진행하고 있는 멀티에디터 프로젝트는, Session Layer 와 Engine Layer 가 존재하는 특이한 계층구조를 갖고있고, 각  기능들의 모듈화보다는 Engine Layer 는 순수하게 유지되어야하고(React를 전혀 몰라야한다) UI또한 순수하게 화면을 그리는것만 알고있어야한다. 물론 다른계층들도 마찬가지이다. 이것이 섞이는 순간 Part.1 에서 말했듯, 데이터가 꼬이고, 그렇게되면 겉잡을 수 없이 프로젝트는 더럽혀지고말것이다. 

 

따라서 이 프로젝트에서는 전통적이고, 수평적 레이어를 잘 드러낼 수 있는 MVC 패턴을 사용하기로 하였다. 

 

모델-뷰-컨트롤러(model–view–controller, MVC)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다. 이 패턴을 성공적으로 사용하면, 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있다. MVC에서 모델은 애플리케이션의 정보(데이터)를 나타내며, 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타내고, 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.

출처: https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC

 

이것은 함께 작업하게 될 개발자 (혹은 미래의 나)를 위한 설계이기도 하다. 일반적인 웹 프론트엔드 프로젝트를 경험한 개발자가 처음 이 프로젝트의 코드베이스를 봤을때, 가장 낯선부분은 아마도 Session Layer 와 Engine Layer일 것이다. 프랙탈 구조에서는 이 계층들이 각 기능 폴더 안에 흩어져 있어, 전체 아키텍쳐의 의도가 드러나지 않는다.

 

# 프랙탈 구조: Session Layer의 존재가 보이지 않는다
domains/video/session/     ← 여기에도 session?
domains/audio/session/     ← 여기에도?
domains/timeline/session/  ← 여기에도?
# → 처음 보는 사람: "session이 뭔지, 왜 모든 도메인에 있는지 모르겠다"

 

반면, 레이어 기반으로 나누는 MVC 방식을 이용한다면, 최상위 폴더 목록만으로도 아키텍쳐의 전체 그림이 보인다. 

# 레이어 기반: 아키텍쳐 의도가 폴더 이름에서 바로 드러난다
src/editor/
├── ui/          → "UI 관련 코드구나"
├── logics/      → "비즈니스 로직이구나"
├── session/     → "세션? 이게 뭐지?" → 열어보면 timeline, playback, history...
├── stores/      → "상태 저장소구나"
└── engine/      → "엔진? 뭔가 무거운 처리를 하는 곳이구나"
# → 처음 보는 사람: 폴더 5개만 봐도 이 프로젝트의 아키텍쳐 컨셉을 이해할 수 있다

 

폴더구조는 아키텍쳐의 첫인상이다. 새로운 개발자가 프로젝트를 처음 열었을 때, 폴더 이름만으로도 "이 프로젝트는 이런 구조로 돌아가는구나" 를 직감할 수 있는 구조가 좋은 구조이다. Session 과 Engine 이라는 특징적인 계층이 최상위에 드러나 있으면, 자연스럽게 "이 프로젝트는 일반적인 CRUD 앱이 아니라 미디어 편집 엔진을 가진 제품이구나" 라는 이해가 생긴다. 아래는 새로운 폴더구조의 예시이다.

 

src/editor/
│
├── ui/                              # Presentation + Component Layer
│   ├── pages/                       # 페이지 진입점
│   ├── layout/                      # 앱 셸 (Header, Sidebar, BottomBar)
│   ├── timeline/                    # 타임라인 UI 컴포넌트
│   ├── preview/                     # 프리뷰 캔버스 UI
│   ├── menu/                        # 메뉴 패널 UI
│   └── shared/                      # 공유 UI 컴포넌트
│
├── logics/                          # Business Layer
│   ├── video/                       # 비디오 비즈니스 로직
│   ├── audio/                       # 오디오 비즈니스 로직
│   ├── text/                        # 텍스트 비즈니스 로직
│   ├── image/                       # 이미지 비즈니스 로직
│   ├── segment/                     # 세그먼트 조작 로직
│   ├── track/                       # 트랙 관리 로직
│   └── export/                      # 내보내기 로직
│
├── session/                         # Session Layer
│   ├── timeline/                    # 타임라인 세션 (재생 위치, 줌, 스크롤)
│   ├── playback/                    # 재생 세션 (재생/정지, 동기화)
│   ├── selection/                   # 선택 세션 (선택된 세그먼트/트랙)
│   └── history/                     # 히스토리 세션 (Undo/Redo)
│
├── stores/                          # Store Layer
│   ├── timelineStore.ts
│   ├── audioEditorStore.ts
│   ├── trackMetadataStore.ts
│   └── ...
│
├── engine/                          # Engine Layer
│   ├── audio/                       # AudioContext, Web Audio API
│   ├── video/                       # 비디오 디코딩, 썸네일 생성
│   ├── canvas/                      # OffscreenCanvas, 합성
│   └── encoder/                     # MediaRecorder, VideoEncoder
│
└── shared/                          # 전역 공유 (타입, 상수, 유틸리티)
    ├── types/
    ├── constants/
    └── utils/

 

4. Business Layer와 Session Layer

4-1. Engine을 누가 호출해야하지?

 

나누고 보니 Business Layer와 Session Layer의 역할이 모호하게 된 점이 있었다. 그 발단은 다음과 같은 질문이었다. 

 

누가 Engine을 호출 할 것이냐?

 

"어느 계층에서 Engine 을 호출해야하지? 둘 다 호출 가능하도록 해야하는건가?" 하는 고민이 되었다. 처음에는 단순히 Session Layer 가 도메인을 횡단하는 계층이니까, Business가 Store를 업데이트하고, Session이 Store를 읽어서 Engine을 호출하게 해야겠다고 생각했다. 

 

하지만 이어서 떠오른 생각. Session 이 Store를 구독한다는것은, Store의 변경이 Session을 트리거하고, Session이 Engine을 호출하는 흐름이 된다. 결국 'Store > Session > Engine' 의 형태로 간접적인 연쇄가 만들어지는데, Store는 순수한 상태저장소여야 한다는 규칙을 어기게 되는것이다. 그렇게 되면 Engine 호출이 어디서 시작된 건지 추적할 수 없게 될 위험이 있었다. 

 

4-2. Business와 Session의 관계

 

이어서 든 생각은, "Business와 Session이 상하 관계인가?" 였다. Component가 상황에 따라 Business 또는 Session을 호출하게 된다. 

또한 재생 중에 세그먼트를 자르는 케이스도 생각해보아야한다. 

Component → business.cutSegment() → Store 업데이트
그런데 지금 재생 중이니까 AudioContext(Engine)도 변경을 반영해야 한다
누가 이걸 트리거하지?

 

Session이 Store를 구독하면 해결되지만, 그건 앞서 말한 Store > Session > Engine 흐름이라 문제가 생긴다. 구독 말고, 다른방법은 없을까? 하던차에, 이 고민이 필요 없는 고민이라는것을 알게되었다. 재생중인 Session은 이미 RAF(RequestAnimationFrame) 루프를 돌고있다. 전에 트림 기능을 개발하였을때, 오디오 재생시 화면 렌더링이 어떻게 이루어지는지에 대해 공부하게되었고, 그때 공부했던 내용이 도움을 주었다. (관련내용은 다음 블로그 글 참고: https://insight74278.tistory.com/1

 

Session Store를 구독하는게 아니라, 이미 실행중인 루프가 Store를 풀링(Pulling)하는 것이니 단방향 흐름이 깨지지 않아 괜찮다. 

 

5. 최종 아키텍쳐 결정

위에서 나열한 내용들과, 추가적으로 소소하게 이름이 바뀐 부분들이 있는데, 아키텍쳐는 환경에 국한되지 않고 모두 적용되는것이어야 한다는 생각에 기존에 Page Layer 라는 부분을 Presentation Layer 라고 명명했고, Service Layer 라는 부분은 대상이 유저인지, 개발자인지에 따라 모호한 이름이 될 수 있을 것 같아 Engine Layer 라는 이름으로 수정하였다. 그렇게 수정된 최종 아키텍쳐는 다음과 같다.  

멀티 미디어 에디터의 최종 아키텍쳐

 

 

더 생각해보아야 할 점은, 비즈니스 로직은 React를 모르고 vanilla zustand만 이용해야한다, react zustand는 UI계층에서만 사용해야한다 에 대한것이다. 이부분은 작업하면서 또 어떻게 변할지 모르겠지만, 우선은 유념해두기로 하였다.  

 

6. 깨닫게 된 점

정리하고 보니 결국 핵심은 세 가지였다. 수평 레이어로 계층을 명확히 나누고, 수직 분할은 폴더 + 규칙으로 충분하며, 폴더 구조가 곧 아키텍처의 첫인상이라는 것. 특히 Session Layer를 분리한 결정은, 단순히 "도메인 횡단 로직을 어디에 둘까"라는 문제를 넘어서 "이 프로젝트가 일반적인 CRUD 앱이 아니라는 걸 어떻게 전달할까"라는 커뮤니케이션 문제이기도 했다.

 

또한 다시 한번 깨닫게 된건, 아키텍쳐는 한 번 정하고 끝이 아니라 기능이 붙을 때 마다 계속 깎아나가야하는 것이라는 점이다. 돌이켜보면, Part1에서 설계한 격자형 아키텍쳐에서 정말 많이 바뀌었다. (어쩌면 그냥 새로운 아키텍쳐가 탄생했다.) 아마 앞으로 작업을 해 나가면서 또 바뀔 것이라 생각한다. 다만, 이번에 확실히 배운 것은, 좋은 아키텍쳐는 "완벽한 구조"가 아니라 "변경의 이유가 명확한 구조"라는 것이다. 결국 개발을 진행하면서 "왜 이렇게 나눠져 있는지"가 이해된다면, 그 아키텍쳐는 제 역할을 하고 있는 거라고 생각한다. 

 

 

 

 

참고:

 

본인 글 (시리즈 내부)

참고 개념/출처

외부 참고 프로젝트

 


← 이전 글: [Part 1] 웹브라우저 멀티에디터 만들기 — 아키텍처 설계 https://insight74278.tistory.com/19