FEConf2025에서 발표한 <그리드 기반 웹 에디터 / 빌더 구현기>를 정리한 글입니다. 그리드 기반 에디터는 웹에 최적화된 자유도를 가진 에디터입니다. 이런 장점을 기반으로 노코드 웹사이트 제작, 사용자 정의 대시보드 등 다양한 제품에서 사용할 수 있는데요. 대표적으로 Squarespace, 큐샵과 같은 서비스에서도 사용하고 있습니다. 본 발표에선 그리드 기반 에디터에 대한 소개와 구현 전략, 구현 시 주의할 점에 대해 다룹니다.
본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다.
미리 요점만 콕 집어보면?
마플코퍼레이션 이선협
"웹사이트 빌더가 필요합니다. 한 달 안에 빠르게 구현할 수 있을까요?"

이 요청을 받았을 때, 솔직히 막막했습니다. 한 달 안에 에디터를 만들고, 그것을 이용해서 빌더를 만들고, 웹사이트를 만들 수 있게 한다는 것은 상식적으로 매우 어려운 일이기 때문입니다. 하지만 일을 안 할 수는 없었기에 여러 방법을 리서치하기 시작했습니다.
우리에게 익숙한 에디터 방식은 크게 세 가지로 나눌 수 있습니다.
첫 번째는 리치 텍스트 기반 에디터입니다. 텍스트 입력을 처리하는 에디터로, TipTap이나 ProseMirror 같은 라이브러리를 사용해 본 경험이 있을 것입니다. 노션이 대표적인 예시인데, 블록 단위로 처리할 수 있어서 텍스트뿐만 아니라 이미지, 테이블 같은 커스텀 요소들을 붙여 웹 페이지처럼 만들 수 있습니다. 실제로 노션 기반의 웹 페이지나 이력서를 만드는 경우도 많습니다.

두 번째는 블록 기반 에디터입니다. 웹의 동작 원리대로 처리하는 방식으로, 웹의 렌더링 규칙에 따라 페이지를 만듭니다. 대부분의 웹 빌더 서비스가 이 형태를 따르는데, HTML과 CSS를 이해해야 한다는 진입 장벽이 있습니다. 하지만 디자인 결과물을 거의 유사하게 구현할 수 있다는 큰 장점이 있습니다.

세 번째는 포지션 기반 에디터입니다. 피그마, 슬라이드처럼 디자인 작업이나 도식 작업에 많이 쓰이는 방식입니다. 제약 없이 자유롭게 배치할 수 있다는 장점이 있지만, 웹사이트를 만들기에는 적절하지 않다는 문제가 있습니다.

이 세 가지 방식 모두 구현이 어렵거나 사용자가 쓰기 어렵다는 문제를 안고 있었습니다. 그래서 고민했습니다. 구현하기 쉬우면서도 사용자가 편하게 쓸 수 있는, 그런 에디터를 만들 수는 없을까?
결론적으로 제가 선택한 방식은 그리드 기반 에디터였습니다.

그리드 기반 에디터는 말 그대로 행과 열로 나누어진 각 셀 안에 요소를 배치하는 방식의 에디터입니다. 스퀘어스페이스(Squarespace)나 Retool 같은 해외 서비스들이 이 방식을 사용하고 있고, 국내에도 몇몇 업체가 이런 형태로 웹사이트 빌더를 제공하고 있습니다.
그리드 기반 에디터의 가장 큰 장점은 직관적인 규칙과 쉬운 구현이 만났다는 점입니다. 행과 열로 나뉘어 있기 때문에 사용자는 어디에 어떻게 배치할지 직관적으로 알 수 있고, 규칙을 따로 설명하지 않아도 이해하기 쉽습니다. 단점이 있다면 자유도가 생각보다 제한적일 수 있다는 점인데, 웹사이트 빌더라는 목적에는 충분했습니다.
그리드 에디터를 구현한다고 하면, 움직이거나 배치할 때 복잡한 좌표 값을 계산해야 할 것 같다고 생각할 수 있습니다. 하지만 생각보다 시작은 굉장히 쉽습니다.그냥 CSS 속성을 이용하면 됩니다.
`display: grid`를 사용하면 그리드 배치를 간단하게 구현할 수 있습니다. 컬럼의 수와 로우의 수를 CSS 속성으로 정의하고, 각 그리드 사이의 간격도 스타일로 설정할 수 있습니다. 그리고 각 요소의 위치 역시 스타일 하나로 배치가 가능합니다. 포지션 기반처럼 top, left를 계산하고 너비와 높이를 일일이 계산하는 것보다 훨씬 직관적이고 쉽습니다. 스타일 속성만으로도 대부분의 배치를 구현할 수 있다는 것이 핵심입니다.

하지만 아직까지는 이것을 에디터라고 부르기 어렵습니다. 그냥 HTML과 CSS의 덩어리일 뿐입니다.
HTML과 CSS가 아닌, 블록들의 위치나 그리드 속성을 데이터로 나타낼 필요가 있습니다. 에디터를 다룰 때 자주 나오는 용어가 AST(Abstract Syntax Tree)인데, 한 줄로 요약하면 보여줄 것들을 트리 구조로 데이터화한다고 보면 됩니다. 일종의 자료 구조이며, 편의를 위해 JSON으로 많이 구현합니다.
에디터를 만들 때 중요한 것은 화면에 어떤 것을 보여줄 것이고, 그 보여주는 것을 어떻게 데이터로 나타낼 것인가입니다. 예를 들어, 블록 세 개가 있는 에디터를 어떻게 데이터화할 수 있을까요? 추상화를 해야 합니다. 전체적으로 보면 컬럼의 수, 로우의 수, 로우의 높이, 각 셀의 간격 등을 데이터화할 수 있습니다. 블록 하나하나도 어떤 타입인지, 어느 위치에 있고 어느 크기를 가지는지, 겹쳐질 수 있다면 z-index는 어느 정도를 가져야 하는지를 수치로 나타낼 수 있습니다.

더 나아가면 각 블록의 타입이 존재한다면, 블록에 attribute를 따로 정의할 수 있습니다. 예를 들어 배경색은 무엇인지, 안에 들어갈 텍스트는 무엇인지 같은 것들을 데이터로 나타낼 수 있습니다. 이러한 데이터들을 받아서 처리하는 특정 함수나 클래스가 바로 렌더러입니다. 결론적으로 JSON 데이터가 화면으로 변환될 수 있도록 로직을 만들어야 합니다.
하지만 아직까지도 이것은 에디터라고 보기 어렵습니다. 어떻게 보면 단순히 state-driven UI처럼 보일 수도 있고, 그냥 컴포넌트 렌더링을 데이터로 나타낸 정도로 볼 수 있습니다. 상호작용이 있어야 비로소 에디터가 됩니다. 사람이 쓰는 것이기 때문에 사람과 시스템의 상호작용을 어떻게 할 것인가를 잘 정리해야 합니다. 그리고 내가 편집한 것이 그대로 실제 화면이 될 수 있도록 WYSIWYG(What You See Is What You Get)을 지원하면 좋습니다.
결국 UI 이벤트를 잘 처리하는 것이 중요하고, 자연스러운 UI/UX를 구현할 수 있어야 합니다.
전체적인 에디터 동작의 흐름을 구조화해 보면 다음과 같습니다.
1. 사람이 어떤 행동을 통해 UI 이벤트를 발생시킵니다.
2. 또는 프로그램이 API를 호출해서 액션을 일으킵니다.
3. 액션이 일어나면 데이터가 업데이트됩니다.
4. 업데이트된 데이터를 다시 렌더링합니다.
이 사이클이 에디터 동작의 기본 흐름입니다. 이를 좀 더 자세히 나누면, 외부 행동(사람의 입력)과 에디터 로직을 엄격하게 구분해야 합니다. 이 두 가지가 섞이게 되면 이동하면서 업데이트하고, 업데이트하면서 UI 렌더링도 하고, UI 이벤트 처리도 하면서 스파게티 코드가 되어버립니다. 그렇기 때문에 처음부터 설계할 때 철저하게 구분해 놓고 진행하는 것이 좋습니다.

에디터는 매우 결정론적이고 룰 기반으로 동작하는 소프트웨어입니다. 룰에 없는 것은 해서는 안 된다는 것이 기본 기조입니다. 블록은 드래그로 이동할 수 있고, 클릭하면 선택할 수 있는 등의 룰을 미리 정리해 놓으면 좋습니다. 할 수 있는 것과 없는 것을 명확히 정리할 필요가 있습니다.
어떤 룰이 필요한가를 먼저 따져봐야 합니다. 편집 가능한가? 이동할 수 있는가? 크기를 바꿀 수 있는가? 이런 상식적인 것들을 기본 룰로 가져갈 수 있고, 만드는 서비스에 따라 세부적으로 달라질 수 있습니다.
UI 이벤트를 처리하는 구조는 다음과 같이 설계할 수 있습니다:
1. UI 이벤트를 받는 리스너가 있어야 합니다.
2. 리스너가 적절한 핸들러에게 전파합니다.
3. 핸들러들이 처리하면서 액션을 발동합니다.
이 과정에서 최적화를 위해 throttle 처리를 하면 좋습니다. 예를 들어, 마우스 무브 이벤트가 발생할 때마다 매번 계산하면 성능적으로 좋지 않습니다. 나중에 블록이 많아지면 끊기는 문제가 생길 수 있습니다.

핸들러에서 처리할 때는 바로바로 액션이 일어나기보다는, 행동이 끝났을 때 액션이 일어나도록 만들어야 합니다. 그렇기 때문에 원본 데이터에서 가져온 데이터로 별도의 상태를 두어 프리뷰 계산을 해야 합니다. 예를 들어, 블록을 드래그해서 이동할 때, 지금은 실제로 이동하는 것처럼 보이지만 실제 데이터 업데이트는 일어나지 않고 프리뷰만 보여주는 상태입니다. 실제로 마우스를 놓으면 그때 데이터 업데이트가 발생하는 구조입니다.
프리뷰를 계산하는 과정에서 룰 위반 검사를 합니다. 이 행동이 적절한가 아닌가를 판단하는 것입니다. 룰 위반 검사를 통해 프리뷰로 "이것은 불가능하다"라는 것도 알려줄 수 있습니다. 그리고 최종적으로 프리뷰 렌더링을 합니다. 행동이 끝나면 그때서야 액션으로 넘어갑니다.

FLIP 애니메이션으로 자연스러운 전환 구현하기
이벤트가 종료되고 액션으로 넘어갈 때, 그냥 바로 넘어가 버리면 굉장히 어색한 경우가 있습니다. 상태 변화를 알려주기 위해 애니메이션 처리를 하면 좋은데, 이때 FLIP 애니메이션 방식을 사용하면 편하게 처리할 수 있습니다.
FLIP은 First, Last, Invert, Play의 약자로:
1.First: 대상의 초기 위치와 크기를 기록합니다.
2. Last: 업데이트 후의 위치와 크기를 기록합니다.
3. Invert: 변화량을 계산합니다.
4. Play: transform으로 애니메이션을 재생합니다.
구현이 굉장히 간단합니다. 초기 위치를 기록하고, 데이터 업데이트 후 최종 위치를 기록한 다음, 변화량을 계산해서 transform으로 애니메이션을 실행하면 됩니다. 이런 식으로 사용자 입력과 프리뷰, 그리고 행동이 끝났을 때 어떻게 처리해야 하는지 구조화할 수 있습니다.

이제 실제로 액션을 처리하는 부분을 다뤄보겠습니다. 에디터에게 명령을 내리는 것은 사람이 직접 상호작용하거나 API 접근을 통한 함수 호출로 최종 데이터를 만들어내는 행위입니다. 이 부분은 UI를 건드는 것보다 코드도 짧고 구현은 쉬운 편입니다.
하지만 신경 써야 할 것이 몇 가지 있습니다.
액션 부분을 좀 더 펼쳐보면 다음과 같습니다:
1. UI 이벤트를 받으면 핸들러가 커맨드를 발행합니다.
2. 커맨드 패턴 형태로 만들어집니다.
3. 이 커맨드들을 모아서 실행시켜 주는 객체가 있습니다.
4. 이 일련의 과정이 하나의 작업 단위가 됩니다.
구체적인 순서는:
1. 이벤트에 대한 명령 묶음 (한 번에 여러 커맨드가 있을 수 있습니다)
2. 명령을 순차적으로 처리
3. 명령 처리 결과를 사용자에게 알림
이 전체 영역을 하나의 작업 단위, 즉 트랜잭션이라고 부릅니다. 트랜잭션은 백엔드에서 많이 쓰는 용어이긴 하지만, 여기서는 하나의 작업 단위로서 commit과 rollback이 가능한 시스템을 만들었다고 생각하면 됩니다.

트랜잭션을 두는 이유는 성공과 실패를 처리하고, 하나의 작업으로 묶기 위함입니다. 예를 들어, 블록을 이동하고 리사이즈를 같이 처리하는데, 그 명령 중 하나가 룰 위반을 했다고 가정해 봅시다. 그러면 한 번에 다 롤백을 해줘야 합니다. 그렇기 때문에 미리 트랜잭션을 걸어놓고, 이 명령들은 하나의 묶음이며 이 중 하나만 실패해도 다 원복해야 한다는 개념으로 접근합니다.
이러한 작업 단위가 있기 때문에 Undo/Redo를 구현하기도 편하고, 커스텀 로직으로 확장하기도 좋습니다.
명령에 대한 결과를 알려주는 것도 굉장히 중요합니다. 에디터 외부에 툴바를 두거나 편집 UI를 구현해야 할 때, 이러한 훅이 없다면 구현할 수가 없습니다. 이는 확장성을 위해 필요합니다.

에디터를 만들다 보면 여러 가지 피드백을 받게 됩니다:
이런 피드백은 대부분 UI적인 표현이 부족했기 때문입니다.
예를 들어:
앞서 설정한 룰을 기반으로 어떤 규칙과 변화를 UI/UX에 녹여야 합니다:
1. 규칙을 암시하는 표현

2. 상태 변화를 명확하게 표현
정리하면, 편집자는 규칙을 암시하는 프리뷰를 보면 더 잘 이해할 수 있고, 현재 상태를 명확하게 보여주고 transition으로 상태 변화를 알려주는 것이 중요합니다. 에디터는 룰 기반 소프트웨어이기 때문에 그 룰을 자세히 알려주지 않으면 어떻게 써야 할지 모를 수 있습니다. 하지만 구구절절 설명하면 서비스가 안 되기 때문에, UI/UX에 잘 녹이는 것이 중요합니다.
에디터를 제품에 활용하기 위해서는 확장이 필요합니다. 다양한 블록들을 표현할 필요가 있고, 타입 안정성에 기대고 싶을 때가 있습니다. 이런 경우 Extension을 별도로 등록할 수 있게 해서 확장하는 것이 가능합니다.

예를 들어, 에디터가 Box 익스텐션을 가진다, Text 익스텐션을 가진다, Image를 가진다는 식으로 표현할 수 있습니다. Extension을 정의할 수 있게 객체를 만들어주고, 이를 상속받아 넣을 수 있게 해줍니다. 이렇게 하면 디폴트 값은 무엇이고, 타입은 무엇이고, 어떻게 렌더링될 것이고, 이 블록은 어떤 attribute를 가질 수 있는지를 타입으로 명확히 할 수 있습니다. 필요한 블록들을 따로따로 정리해서 확장하면 대시보드 같은 특수한 페이지나 복잡한 형태도 만들 수 있습니다.

에디터나 빌더를 구현할 때 주의할 점들이 있습니다. 저도 이 중 거의 다 겪어봤고, 겪으면서 이런 문제들이 있을 수 있다는 것을 깨달았습니다. 실제로 보안 문제를 일으키는 것을 배포하지는 않았지만, 작업 중에 문제가 될 수 있겠다는 것을 많이 느꼈습니다.
주의할 점:
대응 방법:
임시 저장과 용량 관리 전략
좋은 UX를 위해 임시 저장을 많이 지원하는데, 주의할 점이 있습니다.
LocalStorage 사용 시:
서버 임시 저장:
정말 주의가 필요한 부분입니다. 잘못 설정하면 작업했던 것이 다 날아갈 수도 있습니다. 보통 신경 쓰지 않다가 갑자기 문제가 발생하는 경우가 있습니다.
주의할 점:
대응 방법:
여러 사용자가 한 문서에서 작업하다 보면 저장 충돌이 발생할 수 있습니다. 처리를 안 하면 어떤 사용자의 작업은 그냥 무시되어 유실되는 경우가 있습니다.
서비스 특성에 따라 전략을 선택해야 합니다:
1) Last Write Wins (최종 승리)
2) 동시 편집 (가장 어려움)
3) 잠금 정책 (Lock Policy)
4) 3-Way Merge
가장 좋은 것을 선택하면 좋겠지만, 여건이 안 된다면 이 전략에 따라 선택하는 것도 중요합니다.
이번 발표에서 다루지 못한 내용들도 있습니다.
지금까지 설명한 내용과 예제를 직접 체험해 보고 싶다면, 스토리북으로 만든 예제가 있습니다. 부랴부랴 만들어서 완성하지는 못했고, 추후에 다양한 예제와 기능을 보완해서 라이브러리를 배포해 볼 생각입니다. 만약 이런 라이브러리가 이미 있었다면 이렇게까지는 안 했을 텐데, 작년에 이것을 만들고 올해 다시 찾아봤지만 아직도 비슷한 것은 없더라고요. Grid Stack이라는 라이브러리도 있긴 하지만, 웹 빌더가 될 수 있는 정도로 기능을 지원하지는 않습니다.
이 발표가 대체로 설계나 주의할 점, 트러블슈팅 정도만 다뤘기 때문에 코드로 어떻게 표현해야 할지 잘 모르겠다고 생각할 수 있습니다. 저도 그 부분이 걱정되어서 실험을 해봤습니다. 앞서 오픈소스로 공개한 코드의 90%는 바이브 코딩으로 구현했습니다. 물론 그냥 무작정 한 것은 아니고, 발표에서 설명한 설계와 주의할 점을 프롬프트로 안내해 주고, 세부적인 부분도 프롬프트로 신경 쓰면서 구현했습니다. 그랬더니 정말 잘 만들어졌습니다.
만약 여러분도 비슷한 것을 만든다면, 시작을 바이브 코딩으로 해볼 수 있을 것 같습니다. 그런 다음 세부적인 요소들을 직접 건드려가면서 만들어 보길 바랍니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.