🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

A

AI Generated

2026. 1. 31. · 12 Views

Canvas + A2UI 시스템 완벽 가이드

AI가 UI를 직접 제어하는 혁신적인 A2UI(Agent-to-UI) 시스템의 핵심 개념과 Canvas 메커니즘을 실무 관점에서 상세히 다룹니다. 스냅샷 관리부터 커스텀 위젯 제작까지 단계별로 학습합니다.


목차

  1. AI가 UI를 제어한다면
  2. A2UI(Agent-to-UI) 개념
  3. src/canvas-host 코드 분석
  4. Canvas push/reset/eval 메커니즘
  5. 스냅샷과 상태 관리
  6. 실전: 커스텀 Canvas 위젯 만들기

1. AI가 UI를 제어한다면

어느 날 김개발 씨는 회사에서 흥미로운 프로젝트를 맡게 되었습니다. "AI 에이전트가 직접 UI를 조작할 수 있는 시스템을 만들어보세요." 처음에는 도대체 무슨 말인지 이해가 되지 않았습니다.

AI가 UI를 제어한다니, 이게 가능한 일일까요?

A2UI(Agent-to-UI)는 AI 에이전트가 사용자 인터페이스를 직접 제어할 수 있게 해주는 혁신적인 시스템입니다. 마치 사람이 화면을 조작하듯이, AI가 버튼을 추가하고 내용을 바꾸고 레이아웃을 변경할 수 있습니다.

이를 통해 동적이고 지능적인 사용자 경험을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// Canvas를 통한 기본적인 UI 제어
interface CanvasElement {
  type: 'button' | 'text' | 'input';
  props: Record<string, any>;
  children?: CanvasElement[];
}

// AI가 UI를 동적으로 생성
function createDynamicUI(): CanvasElement {
  return {
    type: 'button',
    props: {
      label: 'AI가 생성한 버튼',
      onClick: () => console.log('클릭!')
    }
  };
}

김개발 씨는 선배 개발자인 박시니어 씨를 찾아갔습니다. "선배님, A2UI가 정확히 뭔가요?

AI가 UI를 제어한다는 게 이해가 잘 안 돼요." 박시니어 씨는 웃으며 대답했습니다. "좋은 질문이에요.

먼저 기존 방식을 생각해봅시다." 기존의 UI 개발 방식은 어땠을까요? 전통적인 웹 개발에서는 개발자가 모든 UI를 미리 정의해둡니다.

버튼의 위치, 텍스트의 내용, 입력 필드의 개수까지 모두 코드로 작성됩니다. 사용자는 이미 만들어진 UI를 단순히 사용할 뿐입니다.

마치 정해진 메뉴만 주문할 수 있는 식당과 같습니다. 하지만 AI 시대에는 상황이 달라졌습니다.

사용자가 "내일 날씨 알려줘"라고 물어보면, AI가 단순히 텍스트로 답변하는 것이 아니라 날씨 카드를 직접 화면에 그려줄 수 있다면 어떨까요? 혹은 "할 일 목록 만들어줘"라고 하면, AI가 체크박스와 입력 필드를 동적으로 생성해준다면요?

바로 이것이 A2UI의 핵심 아이디어입니다. A2UI는 Agent-to-UI의 약자입니다.

여기서 Agent는 AI 에이전트를 의미합니다. 쉽게 말해, AI가 마치 프론트엔드 개발자처럼 UI를 직접 조작할 수 있게 만드는 시스템입니다.

비유하자면, A2UI는 마치 요리사가 손님의 취향을 듣고 즉석에서 새로운 요리를 만들어내는 것과 같습니다. 메뉴판에 없는 요리라도 재료가 있다면 바로 만들어낼 수 있습니다.

A2UI도 마찬가지로, 미리 정의되지 않은 UI라도 AI가 필요에 따라 즉석에서 만들어낼 수 있습니다. 왜 이런 시스템이 필요할까요?

첫째, 개인화된 경험을 제공할 수 있습니다. 모든 사용자에게 똑같은 UI를 보여주는 것이 아니라, 각 사용자의 상황과 요구에 맞춘 인터페이스를 동적으로 생성할 수 있습니다.

둘째, 개발 속도가 획기적으로 빨라집니다. 모든 경우의 수를 미리 코드로 작성할 필요가 없습니다.

AI가 상황에 맞게 필요한 UI를 생성해주기 때문입니다. 셋째, 자연어로 UI를 제어할 수 있습니다.

사용자가 "여기에 파란색 버튼 하나 추가해줘"라고 말하면, AI가 이를 이해하고 실제로 UI에 반영할 수 있습니다. 하지만 여기서 중요한 질문이 생깁니다.

"AI가 UI를 제어한다면, 어떻게 안전하게 제어할 수 있을까요?" 잘못된 UI를 생성하거나, 시스템을 망가뜨릴 수도 있지 않을까요? 바로 이 문제를 해결하기 위해 Canvas라는 개념이 등장합니다.

Canvas는 AI가 UI를 안전하게 조작할 수 있는 샌드박스 환경입니다. 마치 어린이가 모래성을 쌓는 모래밭과 같습니다.

아무리 엉망으로 쌓아도 집이나 정원을 망가뜨리지 않습니다. Canvas도 마찬가지로, AI가 UI를 실험하고 수정해도 시스템 전체에는 영향을 주지 않습니다.

실제 현업에서는 어떻게 활용될까요? 예를 들어 고객 지원 챗봇을 만든다고 가정해봅시다.

사용자가 "주문 취소하고 싶어요"라고 하면, AI가 주문 목록 UI를 동적으로 생성하고, 취소 버튼을 추가하고, 확인 다이얼로그까지 보여줄 수 있습니다. 개발자가 미리 모든 UI를 만들어둘 필요가 없습니다.

김개발 씨는 이제 조금씩 이해가 되기 시작했습니다. "아, 그러니까 AI가 필요에 따라 UI를 즉석에서 만들어내는 거군요!" 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 그리고 이를 위해서는 특별한 메커니즘이 필요합니다.

Canvas가 바로 그 핵심이죠."

실전 팁

💡 - A2UI는 정적인 UI가 아닌 동적이고 상황에 맞춘 UI를 제공하는 시스템입니다

  • Canvas는 AI가 안전하게 UI를 조작할 수 있는 샌드박스 환경을 제공합니다
  • 모든 UI 변경은 추적 가능하고 되돌릴 수 있어야 합니다

2. A2UI(Agent-to-UI) 개념

김개발 씨는 A2UI의 전체 구조를 이해하기 위해 공식 문서를 읽기 시작했습니다. 그런데 생소한 용어들이 계속 나왔습니다.

Agent, Canvas, Widget, Snapshot... 도대체 이것들이 어떻게 연결되는 걸까요?

A2UI 시스템은 크게 세 가지 핵심 요소로 구성됩니다. Agent(AI 에이전트)는 UI 변경을 요청하는 주체이고, Canvas는 UI 상태를 관리하는 컨테이너이며, Widget은 실제로 화면에 표시되는 UI 컴포넌트입니다.

이 세 요소가 유기적으로 연결되어 동적인 UI 시스템을 만들어냅니다.

다음 코드를 살펴봅시다.

// A2UI의 핵심 타입 정의
interface Agent {
  id: string;
  // Agent가 UI를 변경하는 메서드
  pushCanvas(elements: CanvasElement[]): void;
}

interface Canvas {
  id: string;
  elements: CanvasElement[];
  // Canvas 상태를 스냅샷으로 저장
  createSnapshot(): Snapshot;
}

interface Widget {
  type: string;
  props: Record<string, any>;
  // Widget을 실제 DOM으로 렌더링
  render(): HTMLElement;
}

박시니어 씨는 화이트보드를 가져와서 다이어그램을 그리기 시작했습니다. "A2UI를 이해하려면 각 구성 요소의 역할을 명확히 알아야 해요." 먼저 Agent부터 살펴보겠습니다.

Agent는 AI의 역할을 담당합니다. 마치 영화 감독과 같습니다.

감독은 직접 연기하지 않지만, 배우들에게 지시를 내려 원하는 장면을 만들어냅니다. Agent도 마찬가지로 직접 UI를 그리지 않지만, Canvas에게 "이런 UI를 보여줘"라고 요청합니다.

Agent는 사용자의 입력을 받아서 분석합니다. 예를 들어 사용자가 "할 일 목록 보여줘"라고 하면, Agent는 이를 이해하고 필요한 UI 구조를 결정합니다.

그리고 Canvas에게 "체크박스 목록을 표시해줘"라고 명령합니다. 다음은 Canvas입니다.

Canvas는 UI 상태를 관리하는 컨테이너입니다. 비유하자면, Canvas는 마치 화가의 캔버스와 같습니다.

화가가 캔버스 위에 그림을 그리고, 마음에 들지 않으면 지우고 다시 그립니다. Canvas도 마찬가지로 Agent가 요청한 UI를 그리고, 필요하면 지우고 다시 그립니다.

하지만 일반 캔버스와 다른 점이 있습니다. Canvas는 모든 변경 사항을 기록합니다.

마치 포토샵의 히스토리 기능처럼, 이전 상태로 되돌릴 수 있습니다. 이를 Snapshot이라고 부릅니다.

마지막으로 Widget입니다. Widget은 실제로 화면에 표시되는 UI 컴포넌트입니다.

버튼, 입력 필드, 카드, 리스트 등이 모두 Widget입니다. 마치 레고 블록과 같습니다.

각 블록은 독립적이지만, 여러 블록을 조합하면 복잡한 구조물을 만들 수 있습니다. Widget은 자신의 props(속성)를 가지고 있습니다.

버튼 Widget이라면 label(버튼 텍스트), onClick(클릭 이벤트) 같은 속성을 가집니다. 입력 필드 Widget이라면 placeholder, value, onChange 같은 속성을 가집니다.

이제 이 세 요소가 어떻게 협력하는지 살펴봅시다. 사용자가 "새 할 일 추가해줘"라고 입력하면, 다음과 같은 과정이 일어납니다.

첫째, Agent가 사용자 입력을 분석합니다. "할 일을 추가하려면 입력 필드와 추가 버튼이 필요하겠군" 하고 판단합니다.

둘째, Agent가 Canvas에게 UI 업데이트를 요청합니다. pushCanvas라는 메서드를 사용합니다.

"입력 필드 하나, 버튼 하나를 화면에 표시해줘"라고 명령합니다. 셋째, Canvas는 현재 상태의 스냅샷을 저장합니다.

나중에 되돌릴 수 있도록 말이죠. 넷째, Canvas는 요청받은 Widget들을 실제로 생성하고 화면에 렌더링합니다.

다섯째, 사용자는 새로 생성된 입력 필드와 버튼을 볼 수 있습니다. 여기서 중요한 점은 단방향 데이터 흐름입니다.

Agent는 Canvas에게만 명령을 보냅니다. Canvas는 Widget을 관리합니다.

Widget은 사용자 이벤트를 받아 다시 Agent에게 전달할 수 있습니다. 이렇게 순환하는 구조를 가집니다.

React를 사용해본 적이 있다면 익숙한 패턴입니다. React에서 부모 컴포넌트가 자식 컴포넌트에게 props를 전달하고, 자식은 이벤트를 통해 부모에게 알립니다.

A2UI도 비슷한 패턴을 따릅니다. 실제 코드에서는 어떻게 구현될까요?

Agent는 보통 LLM(대규모 언어 모델)을 백엔드로 사용합니다. GPT-4나 Claude 같은 AI 모델이 사용자 입력을 분석하고, 필요한 UI 구조를 JSON 형태로 반환합니다.

Canvas는 이 JSON을 받아서 실제 Widget 인스턴스로 변환합니다. 그리고 React나 Vue 같은 프론트엔드 프레임워크를 사용해 화면에 렌더링합니다.

Widget은 재사용 가능한 컴포넌트로 만들어집니다. 개발자는 미리 여러 종류의 Widget을 만들어두고, Agent가 필요에 따라 선택해서 사용할 수 있게 합니다.

김개발 씨는 점점 감이 잡히기 시작했습니다. "그러니까 Agent가 두뇌 역할을 하고, Canvas가 실행자 역할을 하고, Widget이 실제 UI 부품인 거네요!" 박시니어 씨가 웃으며 답했습니다.

"정확해요! 이제 실제 코드를 보면서 더 자세히 알아봅시다."

실전 팁

💡 - Agent는 UI 로직을 담당하고, Canvas는 상태 관리를 담당하며, Widget은 표현을 담당합니다

  • 단방향 데이터 흐름을 유지하면 시스템을 예측 가능하게 만들 수 있습니다
  • Widget은 재사용 가능하게 설계해야 다양한 상황에 활용할 수 있습니다

3. src/canvas-host 코드 분석

김개발 씨는 실제 구현 코드를 보기 위해 프로젝트의 src/canvas-host 폴더를 열었습니다. 여러 파일들이 보였지만, 그중에서도 canvas-manager.ts 파일이 눈에 띄었습니다.

"여기가 핵심이겠군" 하고 생각하며 코드를 읽기 시작했습니다.

canvas-host는 Canvas 시스템의 핵심 구현체입니다. CanvasManager 클래스는 Canvas의 생명주기를 관리하고, Widget 렌더링을 조율하며, 상태 변경을 추적합니다.

이 코드를 이해하면 A2UI 시스템이 실제로 어떻게 동작하는지 알 수 있습니다.

다음 코드를 살펴봅시다.

// src/canvas-host/canvas-manager.ts
class CanvasManager {
  private canvases: Map<string, Canvas> = new Map();
  private snapshots: Map<string, Snapshot[]> = new Map();

  // 새 Canvas 생성
  createCanvas(id: string): Canvas {
    const canvas = new Canvas(id);
    this.canvases.set(id, canvas);
    this.snapshots.set(id, []);
    return canvas;
  }

  // Canvas에 Widget 추가
  pushWidget(canvasId: string, widget: Widget): void {
    const canvas = this.canvases.get(canvasId);
    if (canvas) {
      canvas.addWidget(widget);
      this.saveSnapshot(canvasId, canvas.getState());
    }
  }

  // 스냅샷 저장
  private saveSnapshot(canvasId: string, state: any): void {
    const snapshots = this.snapshots.get(canvasId) || [];
    snapshots.push({ timestamp: Date.now(), state });
    this.snapshots.set(canvasId, snapshots);
  }
}

김개발 씨는 코드를 읽으면서 궁금한 점이 생겼습니다. "왜 Map을 사용했을까?

그냥 배열이나 객체를 쓰면 안 되나?" 박시니어 씨가 설명을 시작했습니다. "좋은 질문이에요.

하나씩 살펴봅시다." 먼저 CanvasManager의 역할을 이해해야 합니다. CanvasManager는 마치 호텔의 매니저와 같습니다.

여러 개의 방(Canvas)을 관리하고, 각 방의 상태를 추적하며, 손님(Agent)의 요청을 처리합니다. 한 번에 여러 Canvas가 존재할 수 있기 때문에, 이를 효율적으로 관리하는 시스템이 필요합니다.

Map 자료구조를 사용했을까요? Map은 키-값 쌍을 저장하는 자료구조입니다.

일반 객체보다 몇 가지 장점이 있습니다. 첫째, 키로 어떤 타입이든 사용할 수 있습니다.

둘째, 순회 성능이 더 좋습니다. 셋째, size 속성으로 크기를 바로 알 수 있습니다.

Canvas를 관리할 때는 ID로 빠르게 찾아야 하는 경우가 많습니다. "이 ID를 가진 Canvas를 찾아라"는 요청이 자주 발생합니다.

Map을 사용하면 O(1) 시간 복잡도로 찾을 수 있습니다. createCanvas 메서드를 자세히 봅시다.

이 메서드는 새로운 Canvas를 생성합니다. 먼저 Canvas 인스턴스를 만들고, canvases Map에 저장합니다.

동시에 snapshots Map에도 빈 배열을 초기화합니다. 왜 이렇게 할까요?

나중에 이 Canvas의 상태 변경 이력을 저장하기 위해서입니다. Canvas가 생성되는 순간부터 스냅샷을 기록할 준비를 해두는 것입니다.

다음으로 pushWidget 메서드를 살펴보겠습니다. 이 메서드는 Agent가 가장 자주 사용하는 메서드입니다.

"이 Widget을 Canvas에 추가해줘"라고 요청할 때 사용됩니다. 코드를 한 줄씩 읽어봅시다.

첫째 줄에서 canvasId로 해당 Canvas를 찾습니다. 만약 존재하지 않는다면?

null이 반환되고 아무 일도 일어나지 않습니다. 이것은 방어적 프로그래밍의 좋은 예시입니다.

둘째 줄에서 Canvas의 addWidget 메서드를 호출합니다. 실제로 Widget을 Canvas에 추가하는 작업이 일어납니다.

셋째 줄이 중요합니다. Widget을 추가한 직후, 현재 상태를 스냅샷으로 저장합니다.

왜 이렇게 할까요? 만약 나중에 "방금 추가한 Widget을 취소하고 싶어"라고 하면 어떻게 할까요?

스냅샷이 있으면 이전 상태로 쉽게 되돌릴 수 있습니다. 마치 게임의 세이브 포인트와 같습니다.

saveSnapshot 메서드는 private입니다. private 메서드는 클래스 내부에서만 사용할 수 있습니다.

외부에서는 직접 호출할 수 없습니다. 왜 이렇게 설계했을까요?

스냅샷 저장은 내부 구현 세부사항입니다. 외부에서 알 필요가 없습니다.

단지 "Widget을 추가하면 자동으로 저장된다"는 것만 알면 됩니다. 이것을 캡슐화라고 부릅니다.

스냅샷은 배열에 저장됩니다. 시간순으로 쌓입니다.

가장 최근 스냅샷은 배열의 마지막에 있습니다. 만약 10번의 변경이 있었다면, snapshots 배열에는 10개의 항목이 있을 것입니다.

실제 프로젝트에서는 스냅샷이 너무 많아질 수 있습니다. 사용자가 Widget을 100번 추가했다면 100개의 스냅샷이 생깁니다.

메모리 문제가 생길 수 있습니다. 따라서 실전에서는 보통 스냅샷 개수를 제한합니다.

예를 들어 최근 20개만 유지하고 나머지는 삭제합니다. 또 다른 최적화 방법은 델타 저장입니다.

전체 상태를 저장하는 대신, 변경된 부분만 저장합니다. "3번 Widget의 label이 'Hello'에서 'Hi'로 변경됨" 같은 식입니다.

훨씬 메모리를 절약할 수 있습니다. 김개발 씨는 코드를 다시 읽어보며 이해한 내용을 정리했습니다.

"CanvasManager는 여러 Canvas를 관리하고, 각 변경 사항을 스냅샷으로 기록하는구나. 그리고 Map을 사용해서 빠르게 찾을 수 있게 했고." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 이제 Canvas의 핵심 메서드들을 더 자세히 알아봅시다."

실전 팁

💡 - Map 자료구조는 ID 기반 조회가 빈번할 때 배열이나 객체보다 효율적입니다

  • private 메서드를 사용해 내부 구현을 숨기고 인터페이스를 단순하게 유지하세요
  • 스냅샷이 너무 많아지지 않도록 개수 제한이나 델타 저장 방식을 고려하세요

4. Canvas push/reset/eval 메커니즘

김개발 씨는 Canvas 클래스의 메서드 목록을 보다가 세 가지 핵심 메서드를 발견했습니다. push, reset, eval.

"이 세 개가 Canvas의 핵심 동작을 담당하는 것 같은데..." 각 메서드가 정확히 무엇을 하는지 궁금해졌습니다.

Canvas는 push(추가), reset(초기화), eval(평가)이라는 세 가지 핵심 메서드를 제공합니다. push는 새로운 Widget을 추가하고, reset은 Canvas를 깨끗한 상태로 되돌리며, eval은 코드를 실행해 동적으로 Widget을 생성합니다.

이 세 메서드를 이해하면 Canvas의 모든 동작을 제어할 수 있습니다.

다음 코드를 살펴봅시다.

// Canvas의 핵심 메서드 구현
class Canvas {
  private widgets: Widget[] = [];

  // Widget을 Canvas에 추가
  push(widget: Widget): void {
    this.widgets.push(widget);
    this.render();  // 화면 다시 그리기
  }

  // Canvas를 비우고 초기 상태로
  reset(): void {
    this.widgets = [];
    this.render();
  }

  // 코드 문자열을 실행해 Widget 생성
  eval(code: string): void {
    try {
      const widgetFactory = new Function('return ' + code);
      const widget = widgetFactory();
      this.push(widget);
    } catch (error) {
      console.error('Widget 생성 실패:', error);
    }
  }

  private render(): void {
    // 실제 DOM 렌더링 로직
  }
}

박시니어 씨는 화면에 코드를 띄워놓고 설명을 시작했습니다. "이 세 메서드는 Canvas의 생명주기를 완전히 제어합니다.

하나씩 깊이 파고들어봅시다." 먼저 push 메서드입니다. push는 가장 직관적인 메서드입니다.

이름 그대로 Widget을 Canvas에 밀어 넣습니다. 마치 장바구니에 상품을 추가하는 것과 같습니다.

한 번 push하면 widgets 배열에 추가되고, 화면에 바로 반영됩니다. 여기서 중요한 점은 불변성입니다.

push는 기존 widgets 배열을 직접 수정합니다. 하지만 더 나은 방법은 새 배열을 만드는 것입니다.

예를 들어 이렇게 말이죠. "this.widgets = [...this.widgets, widget]" 왜 이렇게 할까요?

새 배열을 만들면 상태 변경을 추적하기 쉽습니다. React 같은 프레임워크에서는 참조가 바뀌어야 리렌더링이 일어나기 때문입니다.

push 직후에 render 메서드가 호출됩니다. 이것은 즉시 반영을 의미합니다.

Widget을 추가하자마자 사용자가 바로 볼 수 있습니다. 만약 여러 개의 Widget을 한 번에 추가한다면 어떻게 될까요?

성능 문제가 생길 수 있습니다. push를 10번 호출하면 render도 10번 호출됩니다.

이것은 비효율적입니다. 실전에서는 보통 배치 업데이트를 사용합니다.

여러 변경을 모아서 한 번에 렌더링하는 방식입니다. 다음은 reset 메서드입니다.

reset은 Canvas를 깨끗한 백지 상태로 만듭니다. 모든 Widget이 사라집니다.

마치 칠판 지우개로 칠판을 깨끗하게 지우는 것과 같습니다. 언제 reset을 사용할까요?

몇 가지 상황이 있습니다. 첫째, 사용자가 "처음부터 다시 시작"을 원할 때입니다.

채팅 인터페이스에서 "새 대화 시작" 버튼을 누르면 이전 대화 내용이 모두 사라져야 합니다. 둘째, 에러가 발생했을 때입니다.

Widget 렌더링 중에 문제가 생기면, Canvas를 reset해서 안전한 상태로 돌아갈 수 있습니다. 셋째, 완전히 다른 UI로 전환할 때입니다.

예를 들어 "설정 화면"에서 "메인 화면"으로 바꿀 때, 기존 Widget을 모두 지우고 새로 시작하는 것이 깔끔합니다. reset은 간단해 보이지만 주의할 점이 있습니다.

Widget들이 사용하던 리소스는 어떻게 될까요? 예를 들어 타이머를 실행 중이던 Widget이 있다면, reset 전에 타이머를 멈춰야 합니다.

그렇지 않으면 메모리 누수가 발생합니다. 따라서 실전에서는 reset 전에 cleanup 작업을 수행합니다.

각 Widget의 destroy 메서드를 호출해서 리소스를 정리합니다. 마지막으로 가장 흥미로운 eval 메서드입니다.

eval은 문자열로 된 코드를 실제로 실행합니다. 이것은 굉장히 강력하지만 동시에 위험한 기능입니다.

마치 마법의 주문과 같습니다. 올바른 주문을 외우면 놀라운 일이 일어나지만, 잘못 외우면 재앙이 발생합니다.

왜 eval이 필요할까요? AI Agent가 동적으로 Widget을 생성할 때 유용합니다.

Agent는 코드를 직접 작성할 수 없습니다. 대신 문자열 형태로 "이런 Widget을 만들어줘"라고 요청합니다.

Canvas는 이 문자열을 받아 eval로 실행합니다. 예를 들어 Agent가 이런 문자열을 보냈다고 가정해봅시다.

"{ type: 'button', props: { label: 'Click me', onClick: () => alert('Hi') } }" Canvas는 이 문자열을 eval로 실행해서 실제 Widget 객체를 만듭니다. 굉장히 유연한 방식입니다.

하지만 보안 위험이 있습니다. 만약 악의적인 사용자가 "{ type: 'button', props: { onClick: () => { 모든 데이터 삭제 } } }" 같은 코드를 보낸다면 어떻게 될까요?

시스템이 망가질 수 있습니다. 따라서 eval을 사용할 때는 반드시 샌드박스를 구축해야 합니다.

코드가 할 수 있는 일을 제한하는 것입니다. 파일 시스템 접근 금지, 네트워크 요청 금지, 특정 함수만 사용 가능 등의 규칙을 만듭니다.

또 다른 방법은 화이트리스트 방식입니다. 허용된 Widget 타입만 생성할 수 있게 합니다.

예를 들어 button, input, text만 허용하고 나머지는 차단합니다. 최신 구현에서는 eval 대신 AST 파싱을 사용하기도 합니다.

코드를 실행하지 않고 분석만 합니다. 위험한 코드가 포함되어 있는지 미리 검사할 수 있습니다.

김개발 씨는 노트에 정리했습니다. "push는 추가, reset은 초기화, eval은 동적 생성.

그리고 eval은 조심해서 사용해야 한다." 박시니어 씨가 웃으며 말했습니다. "정확해요.

특히 프로덕션 환경에서는 eval을 최대한 제한적으로 사용하는 것이 좋습니다."

실전 팁

💡 - 여러 Widget을 추가할 때는 배치 업데이트로 렌더링 횟수를 줄이세요

  • reset 전에 각 Widget의 리소스를 정리해서 메모리 누수를 방지하세요
  • eval은 강력하지만 위험하므로 샌드박스나 화이트리스트로 안전하게 제어하세요

5. 스냅샷과 상태 관리

김개발 씨는 코드를 테스트하다가 실수로 잘못된 Widget을 추가했습니다. "아, 이거 어떻게 되돌리지?" 그때 박시니어 씨가 "undo 버튼 눌러보세요"라고 했습니다.

신기하게도 바로 이전 상태로 돌아갔습니다. "어떻게 이게 가능하죠?"

Snapshot은 Canvas의 특정 시점 상태를 저장한 스냅샷입니다. 마치 사진처럼 그 순간의 모든 Widget 정보를 담고 있습니다.

스냅샷을 활용하면 undo/redo 기능, 상태 복원, 시간 여행 디버깅 등 강력한 기능을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// 스냅샷 기반 상태 관리
interface Snapshot {
  id: string;
  timestamp: number;
  widgets: Widget[];
  metadata?: Record<string, any>;
}

class CanvasStateManager {
  private snapshots: Snapshot[] = [];
  private currentIndex: number = -1;

  // 현재 상태를 스냅샷으로 저장
  saveSnapshot(widgets: Widget[]): void {
    // 현재 인덱스 이후의 스냅샷 제거 (새 분기 생성)
    this.snapshots = this.snapshots.slice(0, this.currentIndex + 1);

    const snapshot: Snapshot = {
      id: generateId(),
      timestamp: Date.now(),
      widgets: deepClone(widgets)  // 깊은 복사
    };

    this.snapshots.push(snapshot);
    this.currentIndex++;
  }

  // 이전 상태로 되돌리기
  undo(): Widget[] | null {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.snapshots[this.currentIndex].widgets;
    }
    return null;
  }

  // 다음 상태로 이동
  redo(): Widget[] | null {
    if (this.currentIndex < this.snapshots.length - 1) {
      this.currentIndex++;
      return this.snapshots[this.currentIndex].widgets;
    }
    return null;
  }
}

박시니어 씨는 포토샵 화면을 켜면서 설명을 시작했습니다. "포토샵에서 작업하다가 Ctrl+Z를 눌러본 적 있죠?

그게 바로 스냅샷의 힘입니다." 스냅샷은 시간 여행을 가능하게 합니다. 비유하자면, 스냅샷은 마치 책갈피와 같습니다.

책을 읽다가 중요한 페이지에 책갈피를 꽂아둡니다. 나중에 그 페이지로 바로 돌아갈 수 있습니다.

스냅샷도 마찬가지로 Canvas의 특정 상태에 표시를 해둡니다. 스냅샷에는 무엇이 저장될까요?

가장 중요한 것은 widgets 배열입니다. 현재 Canvas에 있는 모든 Widget의 정보가 저장됩니다.

각 Widget의 타입, props, 위치, 스타일 등이 모두 포함됩니다. 또한 timestamp도 저장됩니다.

언제 이 스냅샷이 만들어졌는지 기록합니다. 이것은 디버깅할 때 유용합니다.

"3초 전에는 정상이었는데 지금은 에러가 나네? 뭐가 바뀐 거지?" metadata 필드도 있습니다.

추가 정보를 저장할 수 있습니다. 예를 들어 "사용자가 버튼을 클릭해서 생긴 변경" 같은 메모를 남길 수 있습니다.

saveSnapshot 메서드를 자세히 봅시다. 먼저 흥미로운 코드가 있습니다.

"this.snapshots.slice(0, this.currentIndex + 1)" 이게 무슨 의미일까요? 이것은 분기 처리를 위한 코드입니다.

예를 들어 이런 상황을 생각해봅시다. 상태 A -> 상태 B -> 상태 C까지 진행했습니다.

그런데 undo를 눌러서 상태 B로 돌아갔습니다. 여기서 새로운 변경을 하면 어떻게 될까요?

상태 D가 생깁니다. 이때 상태 C는 어떻게 될까요?

새로운 타임라인이 만들어진 것이므로 상태 C는 사라져야 합니다. 마치 영화 백 투 더 퓨처처럼 과거를 바꾸면 원래 미래는 사라지는 것과 같습니다.

그래서 slice를 사용해서 currentIndex 이후의 스냅샷을 모두 제거합니다. 깔끔하게 새 분기를 만드는 것입니다.

다음으로 deepClone이 중요합니다. 왜 단순히 widgets를 저장하지 않고 deepClone을 할까요?

얕은 복사와 깊은 복사의 차이를 이해해야 합니다. 만약 그냥 "widgets: widgets"라고 저장하면, 참조만 복사됩니다.

나중에 widgets 배열의 내용이 바뀌면 스냅샷의 내용도 같이 바뀝니다. 이것은 우리가 원하는 동작이 아닙니다.

deepClone은 완전히 새로운 복사본을 만듭니다. 원본이 바뀌어도 복사본은 영향을 받지 않습니다.

마치 사진을 찍는 것과 같습니다. 사진 속 모습은 지금 모습이 바뀌어도 변하지 않습니다.

undoredo 메서드는 쌍둥이 같습니다. undo는 currentIndex를 하나 줄입니다.

시간을 과거로 되돌리는 것입니다. 그리고 그 시점의 widgets를 반환합니다.

Canvas는 이 widgets를 받아서 화면을 다시 그립니다. redo는 반대입니다.

currentIndex를 하나 늘립니다. 시간을 미래로 이동하는 것입니다.

단, 미래가 존재할 때만 가능합니다. 마지막 스냅샷까지 redo하면 더 이상 갈 곳이 없습니다.

경계 조건 검사가 중요합니다. "if (this.currentIndex > 0)" 이 조건은 첫 번째 스냅샷보다 과거로 갈 수 없게 만듭니다.

"if (this.currentIndex < this.snapshots.length - 1)" 이 조건은 마지막 스냅샷보다 미래로 갈 수 없게 만듭니다. 실제 프로젝트에서는 스냅샷 개수를 제한합니다.

무한정 스냅샷을 저장하면 메모리가 부족해집니다. 보통 최근 20~50개 정도만 유지합니다.

오래된 스냅샷은 자동으로 삭제됩니다. 이것을 순환 버퍼라고 부릅니다.

또 다른 최적화는 압축입니다. Widget 정보를 JSON 문자열로 변환하고 gzip으로 압축하면 메모리를 크게 절약할 수 있습니다.

특히 Widget이 많을 때 효과적입니다. 고급 기능으로 선택적 복원도 있습니다.

전체 상태가 아니라 일부만 되돌릴 수 있습니다. 예를 들어 "3번 Widget만 이전 상태로" 같은 식입니다.

더 세밀한 제어가 가능합니다. 김개발 씨는 감탄했습니다.

"스냅샷 하나로 이렇게 많은 걸 할 수 있다니! undo/redo는 생각보다 복잡하네요." 박시니어 씨가 웃었습니다.

"맞아요. 하지만 한 번 제대로 구현해두면 사용자 경험이 엄청나게 좋아집니다.

실수를 쉽게 되돌릴 수 있으니까요."

실전 팁

💡 - 스냅샷은 깊은 복사로 저장해서 원본과 독립적으로 유지하세요

  • 분기 처리를 통해 undo 후 새 변경이 생기면 기존 미래를 제거하세요
  • 메모리 관리를 위해 스냅샷 개수를 제한하고 오래된 것은 삭제하세요

6. 실전: 커스텀 Canvas 위젯 만들기

김개발 씨는 이제 이론은 충분히 이해했다고 생각했습니다. "직접 만들어보고 싶어요!" 박시니어 씨가 미소를 지으며 말했습니다.

"좋아요. 간단한 Todo 위젯을 만들어봅시다.

실전에서 가장 많이 쓰이는 패턴입니다."

커스텀 Canvas Widget을 만들려면 세 가지 요소가 필요합니다. 타입 정의(어떤 Widget인지), 렌더링 로직(어떻게 표시할지), 이벤트 핸들링(사용자 입력 처리).

이 세 요소를 제대로 구현하면 재사용 가능한 강력한 Widget을 만들 수 있습니다.

다음 코드를 살펴봅시다.

// 커스텀 Todo Widget 구현
interface TodoWidgetProps {
  items: string[];
  onAdd: (item: string) => void;
  onToggle: (index: number) => void;
}

class TodoWidget implements Widget {
  type = 'todo';
  props: TodoWidgetProps;

  constructor(props: TodoWidgetProps) {
    this.props = props;
  }

  // Widget을 실제 DOM으로 렌더링
  render(): HTMLElement {
    const container = document.createElement('div');
    container.className = 'todo-widget';

    // 입력 필드와 추가 버튼
    const input = document.createElement('input');
    const addButton = document.createElement('button');
    addButton.textContent = '추가';
    addButton.onclick = () => {
      this.props.onAdd(input.value);
      input.value = '';  // 입력 초기화
    };

    // Todo 목록 렌더링
    const list = document.createElement('ul');
    this.props.items.forEach((item, index) => {
      const li = document.createElement('li');
      li.textContent = item;
      li.onclick = () => this.props.onToggle(index);
      list.appendChild(li);
    });

    container.appendChild(input);
    container.appendChild(addButton);
    container.appendChild(list);

    return container;
  }
}

박시니어 씨는 새 파일을 만들며 말했습니다. "Widget을 만드는 것은 레고 블록을 설계하는 것과 같습니다.

범용적으로 사용할 수 있게 만들어야 합니다." 먼저 타입 정의부터 시작합니다. TodoWidgetProps 인터페이스를 보세요.

이것은 Widget이 받을 수 있는 속성을 정의합니다. 마치 함수의 매개변수 타입을 정의하는 것과 같습니다.

items는 할 일 목록을 담는 배열입니다. string 배열로 정의했습니다.

만약 더 복잡한 정보가 필요하다면 객체 배열을 사용할 수도 있습니다. 예를 들어 "{ id: string, text: string, completed: boolean }" 같은 식입니다.

onAdd와 onToggle은 콜백 함수입니다. Widget은 자체적으로 데이터를 저장하지 않습니다.

대신 상위 컴포넌트에게 "사용자가 이렇게 했어요"라고 알립니다. 이것을 제어 역전(Inversion of Control)이라고 부릅니다.

왜 이렇게 설계할까요? Widget을 무상태(stateless)로 만들기 위해서입니다.

Widget은 표현만 담당하고, 실제 데이터 관리는 외부에서 합니다. 이렇게 하면 Widget을 여러 곳에서 재사용할 수 있습니다.

다음으로 TodoWidget 클래스를 봅시다. "implements Widget"이라는 부분이 중요합니다.

이것은 TodoWidget이 Widget 인터페이스를 따른다는 의미입니다. 즉, render 메서드를 반드시 구현해야 합니다.

type 속성은 'todo'입니다. 이것은 이 Widget의 고유 식별자입니다.

Canvas가 Widget을 구분할 때 사용합니다. "어떤 타입의 Widget인가요?"라고 물으면 "todo예요"라고 답하는 것입니다.

constructor에서 props를 받아서 저장합니다. 나중에 render할 때 이 props를 사용합니다.

간단하지만 중요한 패턴입니다. 이제 핵심인 render 메서드를 자세히 봅시다.

render는 HTMLElement를 반환합니다. 실제로 화면에 표시될 DOM 요소를 만드는 것입니다.

처음부터 차근차근 만들어갑니다. 먼저 container div를 만듭니다.

이것은 모든 요소를 담는 박스입니다. 클래스 이름을 'todo-widget'으로 지정해서 CSS 스타일을 적용할 수 있게 합니다.

다음으로 input과 button을 만듭니다. 사용자가 새 할 일을 입력하는 부분입니다.

버튼의 onclick 이벤트를 주목하세요. "this.props.onAdd(input.value)"를 호출합니다.

이것이 제어 역전의 핵심입니다. Widget은 직접 데이터를 추가하지 않습니다.

대신 "사용자가 이 값을 추가하고 싶어 해요"라고 외부에 알립니다. 그 다음 줄에서 "input.value = ''"로 입력을 초기화합니다.

이것은 UX를 위한 세심한 배려입니다. 사용자가 추가 버튼을 누르면 입력 필드가 비워져야 다음 항목을 입력하기 편합니다.

다음으로 목록 렌더링을 봅시다. forEach로 items 배열을 순회합니다.

각 항목마다 li 요소를 만들고 클릭 이벤트를 붙입니다. 클릭하면 onToggle이 호출됩니다.

"n번째 항목이 클릭되었어요"라고 알리는 것입니다. 여기서 개선할 점이 있습니다.

현재는 완료된 항목과 미완료 항목을 구분하지 않습니다. 실전에서는 completed 상태를 추가하고, 완료된 항목은 취소선을 그어서 표시하는 것이 좋습니다.

마지막으로 모든 요소를 container에 추가하고 반환합니다. 이렇게 만들어진 DOM이 Canvas에 삽입됩니다.

실제로 이 Widget을 사용하는 코드는 어떻게 될까요? Agent가 이런 식으로 Widget을 생성할 것입니다.

"const todoWidget = new TodoWidget({ items: ['우유 사기', '코드 리뷰'], onAdd: (item) => { console.log('추가:', item) }, onToggle: (index) => { console.log('토글:', index) } })" 그리고 canvas.push(todoWidget)으로 Canvas에 추가합니다. 화면에 Todo 위젯이 나타납니다.

확장성을 고려한다면 어떻게 개선할 수 있을까요? 첫째, 스타일을 커스터마이징할 수 있게 만듭니다.

props에 className이나 style 속성을 추가합니다. 둘째, 애니메이션을 추가합니다.

항목이 추가되거나 완료될 때 부드러운 효과를 줍니다. 셋째, 접근성(a11y)을 고려합니다.

키보드 네비게이션, 스크린 리더 지원 등을 추가합니다. 넷째, 에러 처리를 강화합니다.

만약 onAdd가 정의되지 않았다면? 방어 코드를 추가합니다.

김개발 씨는 코드를 직접 타이핑해보며 흥분했습니다. "제 첫 Canvas Widget이네요!

이제 이걸 어떻게 Agent와 연결하죠?" 박시니어 씨가 답했습니다. "Agent는 사용자 요청을 분석해서 적절한 Widget을 선택합니다.

'todo 만들어줘'라고 하면 TodoWidget을 생성하고, '날씨 보여줘'라고 하면 WeatherWidget을 생성하는 식이죠."

실전 팁

💡 - Widget은 무상태로 만들고 데이터 관리는 외부에 맡기세요

  • 콜백 함수로 이벤트를 전달해서 제어 역전 패턴을 구현하세요
  • render 메서드에서 UX를 고려한 세심한 처리를 추가하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#TypeScript#A2UI#Canvas#AgentUI#StateManagement#AI,UI

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.