Design
여기쏙 — Figma plugin 제작기 : 3. 성능
제티Jetty(박정훈) / 웹뷰개발팀여기어때
2025년 12월 18일
원문에서 보기 ↗
0. 프롤로그
이번 글에서는 피그마 플러그인 ‘여기쏙 2.0’ 개발 과정을 중심으로, 극한의 성능 향상을 위해 서비스를 바닥부터 다시 설계한 이야기를 공유하려 합니다.
우선 시간을 되돌려 2024년 7월, ‘여기쏙 1.0’을 런칭했을 때의 기억은 제게 특별하게 남아있습니다. 사용자가 불특정 다수가 아닌, 바로 내 옆자리의 동료 디자이너들이었으니까요. 제가 만든 플러그인이 그들의 업무 시간을 실제로 줄여주는 모습을 바로 옆에서 지켜보는 것, 그리고 기획 단계부터 함께 고민하며 진짜 필요한 기능을 만들어가는 과정은 단순한 기능 구현을 넘어 ‘메이커’로서의 즐거움을 알게 해주었습니다.
그러던 어느 날, 디자이너분들로부터 2.0 리뉴얼 제안이 왔습니다. 망설일 이유가 없었습니다. 1.0 버전을 개발하며 느꼈던 기술적인 아쉬움들, ‘다시 짜면 더 빠르고 견고하게 만들 수 있는데’라는 부채감을 털어버릴 기회였으니까요.
동료들에게는 더 쾌적한 도구를 선물하고, 저에게는 지난 프로젝트의 한계를 넘어 엔지니어로서 한 단계 더 성장하는 계기를 만들기 위해 다시 프로젝트에 합류하게 되었습니다.
하지만 1.0 시절의 레거시 코드는 대규모 데이터 처리에 적합하지 않은 구조였습니다. 초기 코드는 목데이터(Mock Data) 기반의 단순 기능 구현에만 초점이 맞춰져 있어, 수십 개의 비동기 API 호출 과 수백 개의 레이어 조작을 감당하기엔 아키텍처가 턱없이 빈약했기 때문입니다.
단순 기능 업데이트를 넘어, 데이터 처리 효율을 극대화하기 위한 치열한 성능 최적화와 아키텍처 재설계 과정을 기록했습니다.
1. 아키텍처 이해하기: Figma 플러그인은 어떻게 돌아가는가?
본격적인 리팩토링 이야기를 하기 전에, Figma 플러그인의 독특한 구동 방식을 이해할 필요가 있습니다. 웹 프론트엔드 개발자에게는 다소 생소할 수 있는 ‘이중 스레드 구조’ 때문입니다.
Figma 플러그인은 보안과 성능 안정성을 위해 UI 스레드 와 메인 스레드가 철저히 격리된 샌드박스(Sandbox) 환경에서 실행됩니다.

1) UI Thread (iframe)
- 역할: React로 만든 플러그인의 인터페이스(UI)가 돌아가는 곳입니다.
- 특징: 브라우저 API(window, fetch 등)를 쓸 수 있지만, Figma 레이어(Node)에는 직접 접근할 수 없습니다.
2) Main Thread (Sandbox)
- 역할:
code.ts파일이 실행되는 곳으로, Figma의 핵심 엔진 위에서 돌아갑니다. - 특징: 레이어를 조작할 수 있지만, 브라우저 API(DOM, fetch 등)가 없습니다.

2. 통신 구조 개선: 비동기 통신 효율화 (emit → emitPromise)
이 두 스레드는 서로 변수 공유조차 할 수 없는 남남입니다. 유일한 소통 수단은 메시지 패싱뿐입니다. 우리는 이 통신 방식을 3단계로 진화시켰습니다.
1단계: Native API (postMessage)의 한계
Figma가 제공하는 기본 API입니다.
- 구조:
figma.ui.postMessage(Main → UI) /parent.postMessage(UI → Main) - 문제점: 단순한 “던지기” 방식입니다. 메시지를 보내면 끝이고, 이에 대한 응답이 언제, 어떤 메시지로 오는지 연결할 방법이 없습니다. 에러 처리나 타임아웃 같은 고급 기능도 전무합니다.
2단계: 라이브러리 래퍼 (emit/on)의 도입과 좌절
편의성을 위해 @create-figma-plugin/utilities의 emit/on을 도입했습니다. 이벤트 리스너 스타일이라 코드는 깔끔해졌지만, 근본적인 '비동기 응답 매칭' 문제는 여전했습니다.

[문제 상황]
PLP 페이지에서 api.getHotels를 0.1초 간격으로 두 번 호출하면(서울 검색 후 바로 부산 검색), 첫 번째 요청의 리스너가 두 번째 요청의 응답을 가로채는 데이터 뒤섞임이 발생했습니다. 예를 들면 "서울 호텔"을 요청했는데 "부산 호텔"이 화면에 뜨는 끔찍한 버그였죠.
3단계: 최종 솔루션 emitPromise (Request ID 패턴)
우리는 Request ID 패턴을 도입하여 통신 계층을 재설계했습니다. 핵심은 “보낸 요청에 고유 번호표(ID)를 붙이고, 내 번호가 호출될 때만 응답받는 것” 입니다. 이것이 우리가 만든 emitPromise입니다.

emitPromise의 효과:
- 동시성 해결: 100개의 API를 동시에 쏴도
__requestId로 정확히 1:1 매칭됩니다. - Promise 지원: UI 컴포넌트에서
const data = await emitPromise(...)한 줄로 깔끔하게 비동기 처리가 가능합니다. - 안정성: 10초 타임아웃을 두어, 메인 스레드 오류 시 UI가 무한 대기하는 현상을 막았습니다.

3. 렌더링 성능: 데이터 피그마 적용 최적화
통신 문제를 해결하고 나니, 이제는 “그리기(Rendering) 효율” 이 과제였습니다. ILP(목록 페이지) 탭에서 수십개의 객실 옵션이 있는 호텔 아이템 카드들을 선택하고 데이터를 적용할 때, 수많은 레이어 탐색과 고화질 이미지 로딩이 동시에 발생하며 처리 비용이 급증했습니다. 우리는 이를 기술적으로 해결하기 위해 탐색 비용 최소화 와 렌더링 우선순위 분리 전략을 세웠습니다.
성능 저하 요인 1: 반복되는 탐색과 Regex의 늪
기존 코드는 루프를 돌며 카드를 하나씩 처리할 때마다 findOne을 호출하고, 매번 new RegExp 객체를 생성하고 있었습니다. Figma의 findOne은 노드 트리를 깊이 우선 탐색(DFS)하기 때문에, 카드가 많아질수록 기하급수적으로 느려집니다. (O(N x M)의 비효율)
👉 해결: Single Pass Mapping & Regex 상수화
“필요할 때마다 찾기”가 아니라 “진입할 때 지도를 그리기”로 전략을 바꿨습니다.
- Regex 상수화: 정규식 객체를 전역 상수로 선언해 재사용성을 높였습니다.
- Node Mapping: 카드를 순회할 때 단 한 번의
findAll로 필요한 모든 자식 노드(텍스트, 이미지 등)를Map에 담아두었습니다. 이제 노드 접근 속도는 O(1)이 되었습니다.
성능 저하 요인 2: 무거운 이미지 로딩 (Waterfall)
더 큰 병목은 이미지 였습니다. await figma.createImageAsync(url)은 네트워크 요청(다운로드) + 디코딩 + 해싱이 모두 포함된 무거운 작업입니다. 기존 코드는 "텍스트 변경 → 이미지 다운로드(대기) → 다음 카드" 순서로 처리하는 직렬(Waterfall) 구조였습니다.
👉 해결: Text First, Image Last & Smart Caching
사용자가 느끼는 속도(Perceived Performance)를 개선하기 위해 “텍스트와 이미지를 분리”했습니다.
- Text First: 가벼운 텍스트 변경은 즉시
Promise.all로 실행합니다. 버튼을 누르자마자 글자가 촥! 뜹니다. (체감 속도 0.1초) - Image Queue: 이미지는 바로 실행하지 않고 별도의 큐(Queue)에 담아둡니다. 텍스트가 다 뜬 뒤에 백그라운드에서 이미지가 4~6개씩 끊어서 로딩되도록 했습니다.
- Promise Caching: 리스트 특성상 동일한 아이콘이 중복되는 경우가 많았습니다.
Map<URL, Promise>를 이용해 동일한 URL은 네트워크 요청을 단 한 번만 수행하고, 나머지 카드들은 그 결과를 공유받도록 최적화했습니다.
이러한 최적화를 통해 대량의 데이터를 다룰 때 발생할 수 있는 병목 현상을 해소하고, 사용자에게 빠르고 유연하게 반응하는 사용 경험을 제공할 수 있게 되었습니다.
4. UX 고도화: 탭을 넘나드는 데이터 동기화
여기쏙 2.0의 핵심 UX 중 하나는 “PLP(목록)에서 찾은 데이터를 PDP(상세)나 ILP(이미지) 탭에서도 바로 쓸 수 있게 하는 것”이었습니다. 이를 위해서는 탭이 바뀌어 컴포넌트가 언마운트(Unmount) 되어도 데이터가 살아있는 저장소가 필요했습니다.
기술적 과제: 로컬 스토리지 vs 클라이언트 스토리지
웹 개발자라면 흔히 localStorage를 떠올리겠지만, Figma 플러그인 샌드박스 환경에서는 세션이 종료되면 데이터가 불안정해질 수 있습니다. 반면, figma.clientStorage는 사용자의 Figma 계정에 귀속되어 플러그인을 껐다 켜도 데이터가 영구적으로 보존됩니다. 우리는 당연히 후자를 선택했습니다.
하지만 여기서 기술적 난관이 발생합니다.
- 저장소 위치: Main Thread (Figma Engine)
- 데이터 사용: UI Thread (React)
즉, 단순한 데이터 저장/조회조차 스레드를 넘나드는 비동기 통신이 필수 라는 뜻입니다. UI에서 localStorage.setItem() 처럼 동기적으로 처리하려다간 에러를 마주하게 됩니다.
구현: emitPromise를 활용한 데이터 동기화 파이프라인
우리는 앞서 구축한 emitPromise 통신 모듈을 활용해, UI 스레드에서 마치 로컬 변수를 다루듯 간편하게 스토리지를 제어하는 파이프라인을 구축했습니다. 코드는 복잡한 비동기 로직 대신, 아래와 같은 명확한 데이터 흐름으로 정리되었습니다.
- 데이터 생산 (PLP 탭): 디자이너가 데이터를 ‘적용’하는 순간, UI는
emitPromise('setStorage')를 호출해 메인 스레드로 데이터를 던집니다. - 영구 저장 (Main 스레드): 메인 스레드는 받은 데이터를
figma.clientStorage에 비동기로 저장하고, 성공 신호를 보냅니다. - 데이터 소비 (PDP 탭): 탭을 이동하면 새로운 컴포넌트가 마운트되면서
emitPromise('getStorage')를 호출합니다. - 텔레포트 완료: 메인 스레드에서 불러온 최신 데이터가 UI로 전달되고, 즉시 ‘최근 사용 내역’ 리스트에 렌더링 됩니다.
이 구조 덕분에 디자이너는 탭 간의 경계를 느끼지 못합니다. PLP 탭에서 데이터를 적용한 뒤 PDP 탭으로 넘어가면, 이미 그 데이터가 텔레포트 한 것처럼 기다리고 있으니까요.
기술적으로는 격리된 두 스레드 간의 핑퐁 이었지만, 사용자에게는 물 흐르듯 이어지는 하나의 경험을 선물할 수 있게 되었습니다.
마치며: 동료의 시간을 아껴주는 기술의 가치
평소에는 모니터 너머의 수많은 사용자를 위해 코드를 짰다면, 이번 프로젝트는 바로 옆자리의 동료를 위한 것이었습니다. 제가 만든 도구 덕분에 동료들이 “와, 일이 너무 편해졌어요!”라고 말하며 웃을 때, 대고객 서비스 개발과는 또 다른 종류의 짜릿한 보람을 느낄 수 있었습니다. ‘여기쏙 2.0’은 바로 그 웃음을 위해 기존의 한계를 넘어 아키텍처를 바닥부터 다시 쌓아 올린 결과물입니다.
무엇보다 기획 초기부터 디자이너분들과 “어떻게 하면 더 직관적일까?”, “어떤 데이터 흐름이 자연스러울까?”를 치열하게 논의했던 경험은 제게 큰 자산이 되었습니다. 단순히 명세대로 구현하는 것을 넘어, 개발의 언어가 아닌 사용자의 언어로 소통하며 기술을 제품에 녹여내는 법을 배웠기 때문입니다.
또한, 이런 도전이 가능했던 건 업무 효율을 높이는 인터널 툴(Internal Tool) 개발을 적극적으로 지지해 주는 사내 개발 문화 덕분이었습니다. 비즈니스 임팩트뿐만 아니라 동료들의 문제를 해결하는 데 기술을 쓸 수 있도록 응원해 준 팀에게 감사를 전합니다.
앞으로도 제 코드가 동료들의 업무 효율을 높이고, 그들이 더 창의적인 일에 집중할 수 있도록 돕는 든든한 서포터가 되기를 바랍니다.