Excalidraw 및 Fugu: 핵심 사용자 여정 개선

고도로 발달한 기술은 마법과도 같다. 이해하지 못한다면 제 이름은 토마스 슈타이너이고 Google의 개발자 관계팀에서 일하고 있습니다. 이 Google I/O 강연의 글에서는 새로운 Fugu API와 Excalidraw PWA의 핵심 사용자 여정을 개선하는 방법을 살펴봅니다. 이러한 아이디어에서 영감을 얻어 자체 앱에 적용할 수 있습니다.

Excalidraw를 사용하게 된 계기

이야기로 시작하고 싶어. 2020년 1월 1일, Facebook의 소프트웨어 엔지니어인 크리스토퍼 체도가 자신이 작업하기 시작한 작은 그림 앱에 관해 트윗했습니다. 이 도구를 사용하면 만화처럼 손으로 그린 듯한 상자와 화살표를 그릴 수 있습니다. 다음 날에는 타원과 텍스트를 그리고 객체를 선택하여 이동할 수도 있습니다. 1월 3일, 앱의 이름이 Excalidraw로 정해졌고, 모든 좋은 부업 프로젝트와 마찬가지로 도메인 이름을 구매하는 것이 Christopher의 첫 번째 행동 중 하나였습니다. 이제 색상을 사용하고 전체 그림을 PNG로 내보낼 수 있습니다.

직사각형, 화살표, 타원, 텍스트를 지원하는 Excalidraw 프로토타입 애플리케이션의 스크린샷

1월 15일 크리스토퍼는 내 트윗을 비롯해 트위터에서 많은 관심을 받은 블로그 게시물을 게시했습니다. 게시물은 다음과 같은 인상적인 통계로 시작되었습니다.

  • 순 활성 사용자 12,000명
  • GitHub에서 1,500개의 별표
  • 참여자 26명

시작한 지 2주밖에 되지 않은 프로젝트치고는 나쁘지 않습니다. 하지만 내 관심을 진정으로 사로잡은 것은 게시물 아래쪽에 있었습니다. 크리스토퍼는 이번에 새로운 시도를 했다고 썼습니다. 풀 요청을 제출한 모든 사용자에게 무조건 커밋 액세스 권한을 부여한 것입니다. 블로그 게시물을 읽은 당일, 누군가 제출한 기능 요청을 수정하여 Excalidraw에 파일 시스템 액세스 API 지원을 추가하는 풀 요청을 제출했습니다.

PR을 발표하는 트윗의 스크린샷

풀 요청은 하루 후에 병합되었고 그 후로 커밋 액세스 권한이 완전히 부여되었습니다. 말할 필요도 없이, 나는 내 권력을 남용하지 않았어. 지금까지 149명의 참여자 중 누구도 마찬가지입니다.

현재 Excalidraw는 오프라인 지원, 멋진 다크 모드, File System Access API 덕분에 파일을 열고 저장하는 기능까지 갖춘 완전한 설치형 프로그레시브 웹 앱입니다.

오늘날의 상태를 보여주는 Excalidraw PWA의 스크린샷

Excalidraw에 많은 시간을 할애하는 이유에 관한 Lipis의 설명

이로써 'Excalidraw를 사용하게 된 계기'에 관한 이야기는 끝났지만, Excalidraw의 놀라운 기능을 살펴보기 전에 Panayiotis를 소개해 드리겠습니다. 인터넷에서 lipis로 알려진 Panayiotis Lipiridis는 Excalidraw에 가장 많이 기여한 사용자입니다. lipis에게 Excalidraw에 많은 시간을 할애하는 동기를 물었습니다.

다른 사람들과 마찬가지로 크리스토퍼의 트윗을 통해 이 프로젝트에 대해 알게 되었습니다. 첫 번째 기여는 Open Color library를 추가하는 것이었습니다. 이 색상은 오늘날에도 Excalidraw의 일부로 남아 있습니다. 프로젝트가 성장하고 요청이 많아지면서 사용자가 그림을 공유할 수 있도록 그림을 저장하는 백엔드를 구축하는 것이 저의 다음 큰 기여였습니다. 하지만 제가 기여하도록 정말로 이끄는 것은 Excalidraw를 사용해 본 사람은 누구나 다시 사용할 핑계를 찾고 있다는 점입니다.

lipis의 의견에 전적으로 동의합니다. Excalidraw를 사용해 본 사람은 다시 사용할 핑계를 찾고 있습니다.

Excalidraw 사용하기

이제 Excalidraw를 실제로 사용하는 방법을 보여드리겠습니다. 저는 뛰어난 아티스트는 아니지만 Google I/O 로고는 충분히 간단하므로 한번 시도해 보겠습니다. 상자는 'i'이고 선은 슬래시일 수 있으며 'o'는 원입니다. Shift 키를 누른 상태에서 그리면 완벽한 원이 그려집니다. 슬래시를 조금 이동해서 더 보기 좋게 만들겠습니다. 이제 'i'와 'o'에 색상을 지정합니다. 파란색이 좋습니다. 다른 채우기 스타일을 사용해 보세요. 단색인가요, 아니면 크로스 해치인가요? 아니요, 해처링이 멋져요. 완벽하지는 않지만 Excalidraw의 아이디어이므로 저장하겠습니다.

저장 아이콘을 클릭하고 파일 저장 대화상자에 파일 이름을 입력합니다. File System Access API를 지원하는 브라우저인 Chrome에서는 다운로드가 아니라 실제 저장 작업입니다. 파일의 위치와 이름을 선택할 수 있고, 수정하는 경우 동일한 파일에 저장할 수 있습니다.

로고를 변경하고 'i'를 빨간색으로 만들어 줘. 이제 다시 저장을 클릭하면 수정사항이 이전과 동일한 파일에 저장됩니다. 증거로 캔버스를 지우고 파일을 다시 열겠습니다. 수정된 빨간색-파란색 로고가 다시 표시됩니다.

파일 작업

현재 File System Access API를 지원하지 않는 브라우저에서는 각 저장 작업이 다운로드이므로 변경사항을 적용하면 파일 이름에 숫자가 증가하는 여러 파일이 다운로드 폴더를 채우게 됩니다. 하지만 이러한 단점에도 불구하고 파일을 저장할 수는 있습니다.

파일 열기

비밀이 뭐죠? 파일 시스템 액세스 API를 지원할 수도 있고 지원하지 않을 수도 있는 다양한 브라우저에서 열기 및 저장 작업은 어떻게 작동하나요? Excalidraw에서 파일을 여는 작업은 loadFromJSON)(라는 함수에서 이루어지며, 이 함수는 fileOpen()라는 함수를 호출합니다.

export const loadFromJSON = async (localAppState: AppState) => {
  const blob = await fileOpen({
    description: 'Excalidraw files',
    extensions: ['.json', '.excalidraw', '.png', '.svg'],
    mimeTypes: ['application/json', 'image/png', 'image/svg+xml'],
  });
  return loadFromBlob(blob, localAppState);
};

Excalidraw에서 사용하는 제가 작성한 작은 라이브러리인 browser-fs-access에서 제공되는 fileOpen() 함수입니다. 이 라이브러리는 기존 대체를 사용하여 File System Access API를 통해 파일 시스템 액세스를 제공하므로 모든 브라우저에서 사용할 수 있습니다.

먼저 API가 지원되는 경우의 구현을 보여드리겠습니다. 허용되는 MIME 유형과 파일 확장자를 협상한 후 중심이 되는 부분은 File System Access API의 함수 showOpenFilePicker()를 호출하는 것입니다. 이 함수는 여러 파일이 선택되었는지에 따라 파일 배열 또는 단일 파일을 반환합니다. 이제 파일 핸들을 파일 객체에 넣어 다시 가져올 수 있도록 하면 됩니다.

export default async (options = {}) => {
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  const handleOrHandles = await window.showOpenFilePicker({
    types: [
      {
        description: options.description || '',
        accept: accept,
      },
    ],
    multiple: options.multiple || false,
  });
  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
  if (options.multiple) return files;
  return files[0];
  const getFileWithHandle = async (handle) => {
    const file = await handle.getFile();
    file.handle = handle;
    return file;
  };
};

대체 구현은 "file" 유형의 input 요소를 사용합니다. 허용할 MIME 유형과 확장자를 협상한 후 다음 단계는 파일 열기 대화상자가 표시되도록 입력 요소를 프로그래매틱 방식으로 클릭하는 것입니다. 사용자가 하나 이상의 파일을 선택한 경우, 즉 변경 시 프로미스가 해결됩니다.

export default async (options = {}) => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    const accept = [
      ...(options.mimeTypes ? options.mimeTypes : []),
      options.extensions ? options.extensions : [],
    ].join();
    input.multiple = options.multiple || false;
    input.accept = accept || '*/*';
    input.addEventListener('change', () => {
      resolve(input.multiple ? Array.from(input.files) : input.files[0]);
    });
    input.click();
  });
};

파일 저장

이제 저장을 살펴보겠습니다. Excalidraw에서는 saveAsJSON()라는 함수에서 저장됩니다. 먼저 Excalidraw 요소 배열을 JSON으로 직렬화하고, JSON을 blob으로 변환한 다음 fileSave()라는 함수를 호출합니다. 이 함수도 browser-fs-access 라이브러리에서 제공합니다.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: 'application/vnd.excalidraw+json',
  });
  const fileHandle = await fileSave(
    blob,
    {
      fileName: appState.name,
      description: 'Excalidraw file',
      extensions: ['.excalidraw'],
    },
    appState.fileHandle,
  );
  return { fileHandle };
};

먼저 File System Access API를 지원하는 브라우저의 구현을 살펴보겠습니다. 처음 몇 줄은 약간 복잡해 보이지만 MIME 유형과 파일 확장자를 협상하는 것 외에는 아무것도 하지 않습니다. 이전에 저장했고 이미 파일 핸들이 있는 경우 저장 대화상자를 표시할 필요가 없습니다. 하지만 처음 저장하는 경우 파일 대화상자가 표시되고 앱은 나중에 사용할 파일 핸들을 다시 가져옵니다. 나머지는 쓰기 가능한 스트림을 통해 파일에 쓰는 것입니다.

export default async (blob, options = {}, handle = null) => {
  options.fileName = options.fileName || 'Untitled';
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  handle =
    handle ||
    (await window.showSaveFilePicker({
      suggestedName: options.fileName,
      types: [
        {
          description: options.description || '',
          accept: accept,
        },
      ],
    }));
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
  return handle;
};

'다른 이름으로 저장' 기능

이미 있는 파일 핸들을 무시하기로 결정한 경우 '다른 이름으로 저장' 기능을 구현하여 기존 파일을 기반으로 새 파일을 만들 수 있습니다. 이를 보여드리기 위해 기존 파일을 열고, 일부를 수정한 다음, 기존 파일을 덮어쓰지 않고 다른 이름으로 저장 기능을 사용하여 새 파일을 만들겠습니다. 이렇게 하면 원본 파일은 그대로 유지됩니다.

파일 시스템 액세스 API를 지원하지 않는 브라우저의 구현은 원하는 파일 이름이 값이고 블롭 URL이 href 속성 값인 download 속성이 있는 앵커 요소를 만들기만 하면 되므로 짧습니다.

export default async (blob, options = {}) => {
  const a = document.createElement('a');
  a.download = options.fileName || 'Untitled';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', () => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

그런 다음 앵커 요소가 프로그래매틱 방식으로 클릭됩니다. 메모리 누수를 방지하려면 사용 후 blob URL을 취소해야 합니다. 다운로드만 진행되므로 파일 저장 대화상자가 표시되지 않으며 모든 파일이 기본 Downloads 폴더에 저장됩니다.

드래그 앤 드롭

데스크톱에서 제가 가장 좋아하는 시스템 통합 중 하나는 드래그 앤 드롭입니다. Excalidraw에서는 .excalidraw 파일을 애플리케이션에 드롭하면 바로 열리고 수정을 시작할 수 있습니다. 파일 시스템 액세스 API를 지원하는 브라우저에서는 변경사항을 즉시 저장할 수도 있습니다. 필요한 파일 핸들이 드래그 앤 드롭 작업에서 가져왔으므로 파일 저장 대화상자를 거칠 필요가 없습니다.

이를 구현하는 방법은 파일 시스템 액세스 API가 지원될 때 데이터 전송 항목에서 getAsFileSystemHandle()를 호출하는 것입니다. 그런 다음 이 파일 핸들을 몇 단락 위에 나온 loadFromBlob()에 전달합니다. 파일을 열고, 저장하고, 덮어쓰고, 드래그하고, 드롭하는 등 다양한 작업을 할 수 있습니다. 이 모든 트릭과 그 밖의 트릭은 이 도움말에 문서화되어 있으니 너무 빨리 진행된 경우 따라잡으실 수 있습니다.

const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
  this.setState({ isLoading: true });
  // Provided by browser-fs-access.
  if (supported) {
    try {
      const item = event.dataTransfer.items[0];
      file as any.handle = await item as any
        .getAsFileSystemHandle();
    } catch (error) {
      console.warn(error.name, error.message);
    }
  }
  loadFromBlob(file, this.state).then(({ elements, appState }) =>
    // Load from blob
  ).catch((error) => {
    this.setState({ isLoading: false, errorMessage: error.message });
  });
}

파일 공유

현재 Android, ChromeOS, Windows에서 사용되는 또 다른 시스템 통합은 Web Share Target API를 통한 것입니다. Downloads 폴더의 파일 앱에 있습니다. 파일이 두 개 표시되는데 그중 하나는 설명이 없는 이름 untitled와 타임스탬프가 있습니다. 내용을 확인하려면 점 3개를 클릭한 다음 공유를 클릭합니다. 표시되는 옵션 중 하나가 Excalidraw입니다. 아이콘을 탭하면 파일에 I/O 로고만 다시 포함되어 있는 것을 확인할 수 있습니다.

지원 중단된 Electron 버전의 Lipis

아직 설명하지 않은 파일의 기능 중 하나는 파일을 더블클릭하는 것입니다. 파일을 더블클릭하면 일반적으로 파일의 MIME 유형과 연결된 앱이 열립니다. 예를 들어 .docx의 경우 Microsoft Word가 됩니다.

Excalidraw에는 이러한 파일 형식 연결을 지원하는 앱의 Electron 버전이 있었습니다. 따라서 .excalidraw 파일을 더블클릭하면 Excalidraw Electron 앱이 열렸습니다. 이전에 이미 만난 적이 있는 Lipis는 Excalidraw Electron의 제작자이자 지원 중단자였습니다. Electron 버전을 지원 중단할 수 있다고 생각하는 이유를 물었습니다.

사용자들은 처음부터 Electron 앱을 요청해 왔습니다. 주로 파일을 더블클릭하여 열고 싶어했기 때문입니다. 또한 앱을 앱 스토어에 등록할 계획이었습니다. 동시에 PWA를 대신 만들자는 의견도 있어서 두 가지를 모두 진행했습니다. 다행히도 파일 시스템 액세스, 클립보드 액세스, 파일 처리 등 Project Fugu API를 알게 되었습니다. 클릭 한 번으로 Electron의 추가 부담 없이 데스크톱이나 모바일에 앱을 설치할 수 있습니다. Electron 버전을 지원 중단하고 웹 앱에만 집중하여 최고의 PWA를 만드는 것이 쉬운 결정이었습니다. 이제 PWA를 Play 스토어와 Microsoft 스토어에 게시할 수 있습니다. 정말 대단하네요.

Electron이 나빠서가 아니라 웹이 충분히 좋아졌기 때문에 Electron용 Excalidraw가 지원 중단되었다고 말할 수 있습니다. 마음에 듭니다.

파일 처리

'웹이 충분히 좋아졌다'고 말하는 이유는 곧 출시될 파일 처리와 같은 기능 때문입니다.

일반적인 macOS Big Sur 설치입니다. 이제 Excalidraw 파일을 마우스 오른쪽 버튼으로 클릭하면 어떻게 되는지 확인해 보겠습니다. 설치된 PWA인 Excalidraw로 열도록 선택할 수 있습니다. 물론 더블클릭도 작동하지만 스크린캐스트에서 시연하기에는 덜 극적입니다.

그렇다면 어떻게 작동할까요? 첫 번째 단계는 애플리케이션에서 처리할 수 있는 파일 형식을 운영체제에 알리는 것입니다. 웹 앱 매니페스트의 file_handlers라는 새 필드에서 이 작업을 실행합니다. 값은 작업과 accept 속성이 있는 객체의 배열입니다. 작업은 운영체제가 앱을 실행하는 URL 경로를 결정하며 accept 객체는 MIME 유형과 연결된 파일 확장자의 키-값 쌍입니다.

{
  "name": "Excalidraw",
  "description": "Excalidraw is a whiteboard tool...",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "file_handlers": [
    {
      "action": "/",
      "accept": {
        "application/vnd.excalidraw+json": [".excalidraw"]
      }
    }
  ]
}

다음 단계는 애플리케이션이 실행될 때 파일을 처리하는 것입니다. 이는 setConsumer()을 호출하여 소비자를 설정해야 하는 launchQueue 인터페이스에서 발생합니다. 이 함수의 매개변수는 launchParams를 수신하는 비동기 함수입니다. 이 launchParams 객체에는 작업할 파일 핸들의 배열을 가져오는 files라는 필드가 있습니다. 첫 번째 파일만 신경 쓰고 이 파일 핸들에서 블롭을 가져와 이전 친구 loadFromBlob()에 전달합니다.

if ('launchQueue' in window && 'LaunchParams' in window) {
  window as any.launchQueue
    .setConsumer(async (launchParams: { files: any[] }) => {
      if (!launchParams.files.length) return;
      const fileHandle = launchParams.files[0];
      const blob: Blob = await fileHandle.getFile();
      blob.handle = fileHandle;
      loadFromBlob(blob, this.state).then(({ elements, appState }) =>
        // Initialize app state.
      ).catch((error) => {
        this.setState({ isLoading: false, errorMessage: error.message });
      });
    });
}

너무 빨리 진행된 것 같다면 내 도움말에서 파일 처리 API에 대해 자세히 알아보세요. 실험용 웹 플랫폼 기능 플래그를 설정하여 파일 처리를 사용 설정할 수 있습니다. 이 기능은 올해 안에 Chrome에 적용될 예정입니다.

클립보드 통합

Excalidraw의 또 다른 유용한 기능은 클립보드 통합입니다. 그림 전체 또는 일부를 클립보드에 복사하고 원하는 경우 워터마크를 추가한 다음 다른 앱에 붙여넣을 수 있습니다. 참고로 Windows 95 그림판 앱의 웹 버전입니다.

작동 방식은 의외로 간단합니다. 캔버스는 blob으로만 있으면 됩니다. blob이 있는 ClipboardItem가 포함된 단일 요소 배열을 navigator.clipboard.write() 함수에 전달하여 클립보드에 씁니다. 클립보드 API로 할 수 있는 작업에 관한 자세한 내용은 Jason의 내 도움말을 참고하세요.

export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
  const blob = await canvasToBlob(canvas);
  await navigator.clipboard.write([
    new window.ClipboardItem({
      'image/png': blob,
    }),
  ]);
};

export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => {
        if (!blob) {
          return reject(new CanvasError(t('canvasError.canvasTooBig'), 'CANVAS_POSSIBLY_TOO_BIG'));
        }
        resolve(blob);
      });
    } catch (error) {
      reject(error);
    }
  });
};

다른 사용자와 공동작업

세션 URL 공유

Excalidraw에 공동작업 모드가 있다는 사실을 알고 계셨나요? 여러 사용자가 동일한 문서에서 공동작업할 수 있습니다. 새 세션을 시작하려면 실시간 공동작업 버튼을 클릭한 다음 세션을 시작합니다. Excalidraw에 통합된 Web Share API 덕분에 공동작업자와 세션 URL을 쉽게 공유할 수 있습니다.

실시간 공동작업

Pixelbook, Pixel 3a 휴대전화, iPad Pro에서 Google I/O 로고를 작업하여 로컬에서 공동작업 세션을 시뮬레이션했습니다. 한 기기에서 변경한 내용이 다른 모든 기기에 반영되는 것을 확인할 수 있습니다.

모든 커서가 움직이는 것도 확인할 수 있습니다. Pixelbook의 커서는 트랙패드로 제어되므로 안정적으로 움직이지만 Pixel 3a 휴대전화의 커서와 iPad Pro의 태블릿 커서는 손가락으로 탭하여 이러한 기기를 제어하므로 여기저기 움직입니다.

공동작업자 상태 확인

실시간 공동작업 환경을 개선하기 위해 유휴 감지 시스템도 실행됩니다. iPad Pro의 커서를 사용하면 녹색 점이 표시됩니다. 다른 브라우저 탭이나 앱으로 전환하면 점이 검은색으로 바뀝니다. Excalidraw 앱에 있지만 아무것도 하지 않으면 커서에 세 개의 zZZ가 표시되어 내가 유휴 상태임을 나타냅니다.

Google의 간행물을 즐겨 읽는 독자는 유휴 감지가 Project Fugu의 맥락에서 작업한 초기 단계 제안인 유휴 감지 API를 통해 실현된다고 생각할 수 있습니다. 스포일러 주의: 그렇지 않습니다. Excalidraw에는 이 API를 기반으로 하는 구현이 있었지만 결국 포인터 이동과 페이지 가시성을 측정하는 기존 방식을 사용하기로 했습니다.

WICG 유휴 감지 저장소에 제출된 유휴 감지 의견의 스크린샷

유휴 감지 API가 Google의 사용 사례를 해결하지 못하는 이유에 관한 의견을 제출했습니다. 모든 Project Fugu API는 공개적으로 개발되므로 누구나 의견을 제시하고 자신의 의견을 들을 수 있습니다.

Excalidraw를 방해하는 요소에 관한 Lipis의 의견

이와 관련해 Excalidraw를 방해하는 웹 플랫폼에서 누락된 것이 무엇이라고 생각하는지 Lipis에게 마지막 질문을 했습니다.

File System Access API는 훌륭하지만 요즘 제가 중요하게 생각하는 대부분의 파일은 하드 디스크가 아닌 Dropbox나 Google Drive에 있습니다. 파일 시스템 액세스 API에 Dropbox나 Google과 같은 원격 파일 시스템 제공업체와 통합하고 개발자가 코딩할 수 있는 추상화 레이어가 포함되면 좋겠습니다. 그러면 사용자는 안심하고 신뢰할 수 있는 클라우드 제공업체에 파일을 안전하게 보관할 수 있습니다.

lipis에 전적으로 동의합니다. 저도 클라우드에서 생활합니다. 이 기능이 곧 구현되기를 바랍니다.

탭 애플리케이션 모드

와우! Excalidraw에서 정말 멋진 API 통합을 많이 보았습니다. 파일 시스템, 파일 처리, 클립보드, 웹 공유, 웹 공유 타겟 하지만 한 가지 더 있습니다. 지금까지는 한 번에 하나의 문서만 수정할 수 있었습니다. 걱정하지 마세요. Excalidraw에서 탭 형식 애플리케이션 모드의 초기 버전을 처음으로 사용해 보세요. 다음과 같이 표시됩니다.

설치된 Excalidraw PWA에서 기존 파일을 열었으며 독립형 모드로 실행 중입니다. 이제 독립형 창에서 새 탭을 엽니다. 일반 브라우저 탭이 아닌 PWA 탭입니다. 이 새 탭에서 보조 파일을 열고 동일한 앱 창에서 독립적으로 작업할 수 있습니다.

탭형 애플리케이션 모드는 초기 단계에 있으며 아직 확정되지 않은 부분이 있습니다. 관심이 있다면 내 도움말에서 이 기능의 현재 상태를 확인하세요.

마무리

이 기능과 기타 기능에 관한 최신 소식을 확인하려면 Fugu API 추적기를 시청하세요. 웹을 발전시키고 플랫폼에서 더 많은 작업을 할 수 있도록 지원하게 되어 기쁩니다. Excalidraw가 계속 개선되기를 바라며, 여러분이 빌드할 모든 멋진 애플리케이션을 응원합니다. excalidraw.com에서 만들기 시작하세요.

오늘 보여드린 API가 앱에 표시되기를 기대합니다. 제 이름은 톰이고 트위터와 인터넷에서 @tomayac으로 찾으실 수 있습니다. 시청해 주셔서 감사합니다. Google I/O를 계속 즐겨 주세요.