개발2026-02-18

에디터에서 Flexbox와 Absolute 포지셔닝을 함께 다루기: 3-Layer 렌더링 구조의 설계와 그 한계

비주얼 웹 에디터 구현 중 직면하는 가장 까다로운 문제 중 하나는 '에디터의 조작계'와 'CSS 레이아웃'의 간극입니다. 본 글에서는 react-rnd를 활용한 3-Layer 렌더링 구조에서 Flexbox 흐름이 파괴되는 원인을 분석하고, 런타임 포지션 모드 분기를 통해 데이터 정합성을 확보한 트러블슈팅 과정을 공유합니다. WYSIWYG의 핵심인 '보이는 대로 저장되는 시스템'을 구축하기 위한 아키텍처 가이드를 확인해 보세요.

에디터에서 Flexbox와 Absolute 포지셔닝을 함께 다루기: 3-Layer 렌더링 구조의 설계와 그 한계

대상 독자: 웹 기반 에디터(Framer, Figma 등)를 직접 구현해 본 경험이 있거나, React 기반의 복잡한 레이아웃 시스템을 설계해 본 개발자.


배경

WebCreator X는 Framer에서 영감을 받은 비주얼 웹 에디터 프로젝트다. 사용자는 캔버스 위에서 요소를 드래그하거나, 우측 사이드바의 인풋을 통해 스타일을 직접 수정할 수 있다. 이 에디터의 핵심 설계 목표는 두 가지다.

  1. 에디터 기능(드래그, 리사이즈, 선택 테두리)과 실제 결과물 스타일을 완전히 분리한다.
  2. Flexbox 기반의 Stack 레이아웃과 좌표 기반의 Absolute 레이아웃을 동일한 렌더링 파이프라인에서 처리한다.

이 두 목표를 달성하기 위해 모든 노드는 3개의 레이어로 감싸져 렌더링된다.


3-Layer 렌더링 구조

┌─────────────────────────────────────┐
│  Layer 1: EditorNodeWrapper (Rnd)   │  ← 드래그, 리사이즈, 선택 UI
│  ┌───────────────────────────────┐  │
│  │  Layer 2: NodeRenderer        │  │  ← type 기반 컴포넌트 분기
│  │  ┌─────────────────────────┐  │  │
│  │  │  Layer 3: Stack / Text  │  │  │  ← 실제 CSS 스타일 적용
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

각 레이어의 책임은 명확히 분리되어 있다.

레이어 컴포넌트 데이터 소스 역할
Layer 1 EditorNodeWrapper node.layout 에디터 기능 (위치, 크기, 드래그)
Layer 2 NodeRenderer node.type 컴포넌트 분기 (Switch-Case)
Layer 3 Stack, Text, Button node.style 실제 디자인 스타일 렌더링

processNodeStyles 유틸리티는 Layer 3에서 width, height, position 같은 레이아웃 속성을 필터링하여 Layer 1과의 충돌을 방지한다. 이것이 데이터 정합성의 첫 번째 방어선이다.


문제: react-rnd와 Flexbox의 충돌

문제는 Layer 1에서 발생한다. 에디터의 드래그 기능을 위해 react-rnd 라이브러리를 사용하는데, 이 라이브러리는 내부적으로 래퍼 요소에 position: absolute를 강제 주입한다.

Absolute 모드의 노드에서는 이것이 문제가 없다. node.layoutx, y 좌표가 left, top으로 치환되어 의도한 위치에 정확히 배치된다.

그러나 Flex 아이템(Relative 모드) 노드에서는 치명적인 문제가 생긴다.

Stack (display: flex, flex-direction: row)
├── Rnd [position: absolute] ← 플렉스 흐름을 이탈!
│   └── Text "Hello"
└── Rnd [position: absolute] ← 플렉스 흐름을 이탈!
    └── Button "Click"

부모가 Flexbox임에도 불구하고, 자식 Rnd 래퍼들이 absolute로 렌더링되어 플렉스 흐름을 완전히 이탈한다. 결과적으로 두 자식 노드는 (0, 0) 좌표에 겹쳐서 렌더링된다.

이것이 사이드바에서는 position: relative로 설정되어 있음에도 불구하고, 실제 캔버스에서는 absolute처럼 동작하는 괴리의 원인이다.


구현 배경: 왜 이렇게 됐는가

초기 구현에서는 hasRelativePosition이라는 변수를 통해 이 문제를 우회하려 했다.

// 이전 코드 (우회 로직)
const hasRelativePosition =
  !node.style.position || node.style.position === "relative";

<Rnd
  size={hasRelativePosition
    ? { width: "auto", height: "auto" }  // Flex 모드: 크기를 auto로
    : { width, height }                   // Absolute 모드: 고정 크기
  }
  position={hasRelativePosition
    ? undefined                           // Flex 모드: 좌표 전달 안 함
    : { x, y }                           // Absolute 모드: 좌표 전달
  }
  style={{
    position: hasRelativePosition ? "relative" : "absolute",
    display: hasRelativePosition ? "inline-block" : "block",
  }}
/>

이 로직은 react-rndposition prop을 undefined로 두어 라이브러리가 강제로 absolute를 주입하지 못하게 막는 방식이었다. 그러나 이 코드는 이후 리팩토링 과정에서 제거되었고, 현재는 모든 노드가 동일하게 x, y 좌표를 전달받아 absolute처럼 동작하게 되었다.


해결 방안

올바른 해결책은 노드의 포지션 모드를 런타임에 판별하여 Rnd의 동작을 분기하는 것이다.

// 개선된 EditorNodeWrapper 로직
const isAbsoluteMode = node.style.position === "absolute";

<Rnd
  // Absolute 모드: 고정 좌표와 크기를 Rnd에게 전달
  // Flex 모드: 좌표와 크기를 전달하지 않아 플렉스 흐름을 따르게 함
  size={isAbsoluteMode ? { width, height } : { width: "auto", height: "auto" }}
  position={isAbsoluteMode ? { x, y } : undefined}
  disableDragging={!isSelected || !isAbsoluteMode}
  enableResizing={isAbsoluteMode ? (isSelected ? undefined : false) : false}
  style={{
    position: isAbsoluteMode ? "absolute" : "relative",
    display: isAbsoluteMode ? "block" : "inline-block",
  }}
/>

이 변경으로 Layer 1이 node.style.position을 기준으로 두 가지 모드를 명확히 구분하게 된다.


해결 후 가능한 동작

기능 해결 전 해결 후
Flex 아이템 배치 모두 (0,0)에 겹침 부모 Stack의 gap, justify-content 등 Flexbox 규칙에 따라 정상 배치
Absolute 노드 드래그 동작 (단, Flex 아이템도 드래그 가능) Absolute 노드만 드래그 가능
사이드바 ↔ 캔버스 정합성 사이드바 설정값이 캔버스에 반영 안 됨 사이드바 position 설정이 캔버스 동작과 일치
최종 결과물 배포 Flex 레이아웃이 깨진 채로 저장됨 에디터에서 보이는 그대로 배포 가능

핵심 교훈

이 문제의 본질은 "에디터 기능을 위한 레이어"와 "결과물을 위한 레이어"의 책임 경계가 흐려졌을 때 발생하는 데이터 불일치다.

에디터를 만들 때 가장 어려운 부분 중 하나는, 사용자가 조작하는 "에디터의 세계"와 실제로 저장되고 배포되는 "결과물의 세계"를 동기화하는 것이다. 이 두 세계 사이의 번역 레이어(EditorNodeWrapper)가 모든 포지션 모드를 정확히 이해하고 처리해야만 사용자는 "보이는 것이 저장되는 것(WYSIWYG)"을 신뢰할 수 있다.

단방향 데이터 흐름이 정합성의 핵심이다.
사용자 조작 → 스토어 업데이트 → 전체 재렌더링. 이 흐름이 끊기지 않는 한, 어떤 레이어에서 어떤 변환이 일어나더라도 최종 상태는 항상 스토어의 데이터를 기준으로 수렴한다.