고도로 발달한 기술은 모두 마법과도 같다. 도저히 이해가 되지 않는 이상 말이죠. 저는 Google의 개발자 관계팀인 토마스 스타이너입니다. 이번 Google I/O 대담의 글에서는 새로운 Fugu API를 몇 가지 살펴보고 Excalidraw PWA의 핵심 사용자 여정을 개선하는 방법을 살펴보겠습니다. 그래야 이러한 아이디어에서 아이디어를 얻고 자신의 앱에 적용할 수 있습니다.
Excalidraw를 시작하게 된 계기
이야기부터 시작하려고 해요. 2020년 1월 1일, Facebook의 소프트웨어 엔지니어인 크리스토퍼 체도는 자신이 작업하기 시작한 작은 그리기 앱에 관한 트윗을 보냈습니다. 이 도구를 사용하면 손으로 그린 만화 같은 느낌의 상자와 화살표를 그릴 수 있습니다. 다음 날 타원과 텍스트를 그리고 객체를 선택하여 이동할 수도 있습니다. 1월 3일에 이 앱의 이름은 Excalidraw라는 이름을 갖게 되었고 다른 사이드 프로젝트와 마찬가지로 Christopher의 첫 번째 행위 중 하나가 도메인 이름 구매였습니다. 이제 색상을 사용하고 전체 그림을 PNG로 내보낼 수 있었습니다.
1월 15일에 크리스토퍼는 저를 포함하여 Twitter에서 많은 관심을 받은 블로그 게시물을 게시했습니다. 글은 인상적인 통계 기록으로 시작되었습니다.
- 순 활성 사용자 12,000명
- GitHub의 별점 1.5천 개
- 참여자 26명
불과 2주 전에 시작된 프로젝트는 전혀 나쁘지 않습니다. 하지만 저의 관심을 가장 끈 것은 이 게시물의 아래쪽에 있습니다. 크리스토퍼는 이번에는 pull 요청을 받은 모든 사용자에게 무조건 커밋 액세스 권한을 부여하는 새로운 방법을 시도했다고 밝혔습니다. 블로그 게시물을 읽은 당일에 Excalidraw에 File System Access API 지원을 추가해 달라는 pull 요청을 요청하여 누군가가 제출한 기능 요청을 수정했습니다.
내 pull 요청이 하루 후에 병합되고 나서 전체 커밋 액세스 권한을 갖게 되었습니다. 당연히 내 힘을 악용하지 않았어. 그리고 지금까지 149명의 참여자 중 그 누구도 보지 못했습니다.
현재 Excalidraw는 설치 가능한 정식 프로그레시브 웹 앱으로서 오프라인 지원, 놀라운 어두운 모드, File System Access API 덕분에 파일을 열고 저장하는 기능을 제공합니다.
Excalidraw에 그토록 많은 시간을 할애하는 이유에 대한 Lipis
이로써 'Excalidraw의 여정'을 마치겠습니다. 하지만 Excalidraw의 놀라운 기능을 자세히 알아보기 전에 Panayiotis를 소개하게 되어 기쁩니다. 인터넷에서 간단히 lipis라고 하는 Panayiotis Lipiridis는 Excalidraw에 가장 활발히 기여한 인물입니다. 저는 lipis에게 왜 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);
};
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;
};
'다른 이름으로 저장' 기능
기존 파일 핸들을 무시하기로 결정한 경우 '다른 이름으로 저장' 기능을 구현하여 기존 파일을 기반으로 새 파일을 만들 수 있습니다. 이를 보여주기 위해 기존 파일을 열고 수정한 다음 기존 파일을 덮어쓰지 않고, '다른 이름으로 저장' 기능을 사용하여 새 파일을 만들어 보겠습니다. 이렇게 하면 원본 파일은 그대로 유지됩니다.
File System Access API를 지원하지 않는 브라우저를 위한 구현은 짧습니다. 값이 원하는 파일 이름이고 blob 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
파일을 애플리케이션에 드롭하면 즉시 열리고 편집을 시작할 수 있습니다. 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
폴더에 있는 Files 앱이 있습니다. 두 개의 파일이 있습니다. 그 중 하나는 설명이 아닌 이름 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로 만들기는 쉽게 결정할 수 있었습니다. 무엇보다, 이제 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 페인트 앱의 웹 버전입니다.
작동 방식은 놀라울 정도로 간단합니다. 이제 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의 맥락에서 개발된 초기 단계 제안서인 Idle Detection API를 통해 실현된다고 생각할 수 있습니다. 스포일러 주의: 아닙니다. 우리는 Excalidraw에서 이 API를 기반으로 구현했지만, 최종적으로 포인터 이동 및 페이지 가시성 측정을 기반으로 보다 전통적인 접근 방식을 사용하기로 결정했습니다.
Idle Detection API로 기존 사용 사례를 해결하지 못한 이유에 관한 의견을 제출했습니다. 모든 Project Fugu API는 공개 개발이 진행 중이므로 누구나 의견을 내고 의견을 낼 수 있습니다.
엑스칼리드로를 막고 있는 누드의 Lipi
이에 대해 lipis에게 마지막으로 Excalidraw를 보류하는 웹 플랫폼에서 무엇이 빠졌다고 생각하는지 물었습니다.
File System Access API는 훌륭하지만 그렇다는 사실을 알고 계셨나요? 요즘 내가 신경 쓰는 대부분의 파일은 하드 디스크가 아닌 Dropbox나 Google 드라이브에 있습니다. Dropbox나 Google과 같은 원격 파일 시스템 제공업체가 통합하고 개발자가 코딩할 수 있는 추상화 계층이 File System Access API에 포함되어 있으면 좋겠습니다. 그러면 사용자는 신뢰하는 클라우드 제공업체를 통해 안심하고 파일이 안전한지 확인할 수 있습니다.
저도 클라우드에 동의합니다. 이 기능이 조만간 구현되기를 바랍니다.
탭으로 표시된 애플리케이션 모드
잘하셨습니다. 지금까지 Excalidraw에서 뛰어난 API 통합이 많이 확인되었습니다. 파일 시스템, 파일 처리, 클립보드, 웹 공유, 웹 공유 대상 하지만 한 가지 더 있습니다. 지금까지는 한 번에 하나의 문서만 수정할 수 있었습니다. 이제 더 이상 그렇지 않습니다. Excalidraw에서 탭형 애플리케이션 모드의 초기 버전을 처음으로 사용해 보세요. 다음과 같이 표시됩니다.
설치된 Excalidraw PWA에 독립형 모드로 실행되는 기존 파일이 열려 있습니다. 이제 독립형 창에서 새 탭을 엽니다. 이 탭은 일반 브라우저 탭이 아니라 PWA 탭입니다. 그러면 이 새 탭에서 보조 파일을 열고 동일한 앱 창에서 독립적으로 작업할 수 있습니다.
탭 방식의 애플리케이션 모드는 초기 단계이며 모든 것이 정해져 있는 것은 아닙니다. 관심이 있는 경우 내 문서에서 이 기능의 현재 상태를 읽어보세요.
마무리
이 기능 및 다른 기능을 계속 확인하려면 Fugu API 추적기를 시청하세요. 웹을 더욱 발전시켜 플랫폼에서 더 많은 작업을 할 수 있게 되어 정말 기쁩니다. Excalidraw를 지속적으로 개선하고, 개발자가 빌드할 모든 놀라운 애플리케이션을 기원합니다. excalidraw.com에서 만들기를 시작해 보세요.
오늘 보여드린 API 중 일부가 앱에 나타나기를 고대합니다. 제 이름은 톰입니다. 트위터와 인터넷에서 일반적으로 @tomayac입니다. 시청해 주셔서 감사합니다. 나머지 Google I/O도 즐겁게 시청하시기 바랍니다.