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

고도로 발달한 기술은 마법과도 같다. 이해하지 못하는 경우는 예외입니다. 저는 Google의 개발자 관계팀에서 근무하는 토마스 슈타이너입니다. 이 Google I/O 강연의 자료에서는 새로운 Fugu API와 이러한 API가 Excalidraw PWA의 핵심 사용자 여정을 개선하는 방법을 살펴봅니다. 이를 통해 아이디어를 얻고 이를 자체 앱에 적용할 수 있습니다.

Excalidraw를 사용하게 된 계기

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

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

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

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

불과 2주 전에 시작된 프로젝트에 비하면 나쁘지 않은 결과입니다. 하지만 게시물 하단에 있는 내용이 제 관심을 끌었습니다. 크리스토퍼는 이번에 새로운 시도를 했다고 말했습니다. 풀 리퀘스트를 제출한 모든 사용자에게 무조건적인 커밋 액세스 권한을 부여한 것입니다. 블로그 게시물을 읽은 날짜에 Excalidraw에 File System Access API 지원을 추가하여 누군가 제출한 기능 요청을 수정하는 풀 요청을 제출했습니다.

PR을 발표한 트윗의 스크린샷

하루 후 내 풀 요청이 병합되었고 그때부터 전체 커밋 액세스 권한을 갖게 되었습니다. 물론 권력을 남용하지는 않았습니다. 지금까지 149명의 참여자 중 다른 누구도 이 문제를 신고하지 않았습니다.

현재 Excalidraw는 오프라인 지원, 멋진 어두운 모드, File System Access API를 통한 파일 열기 및 저장 기능을 갖춘 완전한 설치형 프로그레시브 웹 앱입니다.

현재 상태의 Excalidraw PWA 스크린샷

리피스님이 Excalidraw에 많은 시간을 할애하는 이유

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

다른 사람들처럼 크리스토퍼의 트윗을 통해 이 프로젝트에 대해 알게 되었습니다. 제가 처음으로 기여한 부분은 오늘날 Excalidraw에 여전히 포함되어 있는 색상인 Open Color 라이브러리를 추가한 것입니다. 프로젝트가 성장하고 요청이 많아지면서 다음으로 큰 기여를 한 부분은 사용자가 공유할 수 있도록 그림을 저장하기 위한 백엔드를 빌드하는 것이었습니다. 하지만 Excalidraw를 사용해 본 사람은 누구나 다시 사용할 구실을 찾고 있다는 점이 저를 참여하게 하는 동기입니다.

리피스님과 완전히 동의합니다. Excalidraw를 사용해 본 사람은 누구나 다시 사용할 핑계를 찾고 있습니다.

Excalidraw 사용하기

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

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

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

파일 작업

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

파일 열기

비결은 무엇일까요? File System Access 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);
};

제가 작성한 작은 라이브러리인 browser-fs-access에서 가져온 fileOpen() 함수로, 이 함수는 Excalidraw에서 사용합니다. 이 라이브러리는 기존 대체 기능이 있는 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;
};

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

기존 파일 핸들을 무시하기로 결정하면 '다른 이름으로 저장' 기능을 구현하여 기존 파일을 기반으로 새 파일을 만들 수 있습니다. 이를 보여주기 위해 기존 파일을 열고 수정한 후 기존 파일을 덮어쓰지 말고 저장 기능을 사용하여 새 파일을 만듭니다. 이렇게 하면 원본 파일이 손상되지 않습니다.

File System Access API를 지원하지 않는 브라우저의 구현은 짧습니다. 원하는 파일 이름이 값인 download 속성과 blob URL이 href 속성 값인 앵커 요소를 만드는 것뿐이기 때문입니다.

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 파일을 애플리케이션에 드롭하면 즉시 열리고 수정을 시작할 수 있습니다. File System Access API를 지원하는 브라우저에서는 변경사항을 즉시 저장할 수도 있습니다. 필요한 파일 핸들이 드래그 앤 드롭 작업에서 가져왔으므로 파일 저장 대화상자를 거칠 필요가 없습니다.

이를 실행하는 비결은 File System Access 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 앱이 열렸습니다. 이전에 이미 만나 뵈었던 리피스는 Excalidraw Electron의 개발자이자 지원 중단자였습니다. Electron 버전을 지원 중단할 수 있다고 생각한 이유를 물었습니다.

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

Electron용 Excalidraw가 지원 중단된 것은 Electron이 나쁘기 때문이 아니라 웹이 충분히 좋아졌기 때문이라고 할 수 있습니다. 마음에 듭니다.

파일 처리

'웹이 충분히 좋아졌습니다'라고 말하는 이유는 향후 제공될 파일 처리와 같은 기능 때문입니다.

일반 macOS Big Sur 설치입니다. 이제 Excalidraw 파일을 마우스 오른쪽 버튼으로 클릭하면 어떻게 되는지 확인해 보겠습니다. 설치된 PWA인 Excalidraw로 열 수 있습니다. 물론 더블클릭해도 됩니다. 단지 스크린캐스트로 보여주기에는 그다지 극적이지 않습니다.

어떻게 작동하나요? 첫 번째 단계는 애플리케이션이 처리할 수 있는 파일 형식을 운영 체제에 알려주는 것입니다. 웹 앱 매니페스트의 file_handlers라는 새 필드에서 이 작업을 실행합니다. 값은 작업과 accept 속성이 있는 객체의 배열입니다. 작업은 운영체제에서 앱을 실행하는 URL 경로를 결정하고 수락 객체는 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 필드가 있습니다. 첫 번째 파일 핸들만 신경 쓰고 이 파일 핸들에서 blob을 가져와서 오래된 친구 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 });
      });
    });
}

너무 빨리 지나갔다면 도움말에서 File Handling API에 관해 자세히 알아보세요. 실험용 웹 플랫폼 기능 플래그를 설정하여 파일 처리를 사용 설정할 수 있습니다. 이 기능은 올해 안에 Chrome에 출시될 예정입니다.

클립보드 통합

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

작동 방식은 놀라울 만큼 간단합니다. 필요한 것은 blob으로 캔버스뿐입니다. 그런 다음 blob이 포함된 ClipboardItem가 있는 1개 요소 배열을 navigator.clipboard.write() 함수에 전달하여 클립보드에 씁니다. 클립보드 API로 할 수 있는 작업에 관한 자세한 내용은 제이슨의 도움말과 제 도움말을 참고하세요.

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 3개로 표시되어 유휴 상태임을 나타냅니다.

Google 게시물을 열심히 읽는 독자는 유휴 감지가 Project Fugu의 맥락에서 작업된 초기 단계의 제안인 Idle Detection API를 통해 실현된다고 생각할 수 있습니다. 스포일러 주의: 그렇지 않습니다. Excalidraw에는 이 API를 기반으로 하는 구현이 있었지만 결국 포인터 움직임과 페이지 가시성을 측정하는 보다 전통적인 접근 방식을 사용하기로 결정했습니다.

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

Idle Detection API가 사용 사례를 해결하지 못하는 이유에 관한 의견을 제출했습니다. 모든 Project Fugu API는 공개적으로 개발되고 있으므로 누구나 참여하여 의견을 제출할 수 있습니다.

Excalidraw의 문제점

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

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

저도 클라우드에 살고 있으므로 리피스님과 완전히 동의합니다. 곧 구현될 수 있기를 바랍니다.

탭형 애플리케이션 모드

와우! Excalidraw에서는 훌륭한 API 통합이 많이 이루어졌습니다. 파일 시스템, 파일 처리, 클립보드, 웹 공유, 웹 공유 대상 하지만 한 가지 더 있습니다. 지금까지는 한 번에 하나의 문서만 수정할 수 있었습니다. 더 이상은 그럴 필요가 없습니다. Excalidraw에서 탭형 애플리케이션 모드의 초기 버전을 처음으로 사용해 보세요. 다음과 같이 표시됩니다.

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

탭형 애플리케이션 모드는 초기 단계이며 아직 모든 사항이 확정되지는 않았습니다. 관심이 있는 경우 도움말에서 이 기능의 현재 상태를 확인하세요.

마무리

이 기능과 기타 기능에 대한 최신 소식을 확인하려면 Fugu API 추적기를 확인하세요. 웹을 발전시키고 개발자가 플랫폼에서 더 많은 작업을 할 수 있도록 지원하게 되어 기쁩니다. 계속해서 개선되는 Excalidraw와 개발자가 빌드할 모든 놀라운 애플리케이션을 응원합니다. excalidraw.com에서 바로 시작해 보세요.

오늘 보여드린 API가 여러분의 앱에 표시되는 모습을 기대하고 있습니다. 저는 톰입니다. 트위터와 인터넷에서 @tomayac으로 검색해 주세요. 시청해 주셔서 감사합니다. 즐거운 Google I/O 시청 되시길 바랍니다.