아무리 진보된 기술도 마법과 구별할 수 없습니다. 당신이 이해하지 않는 한. 저는 Google의 개발자 관계팀에서 일하는 토마스 슈타이너입니다. 이번 Google I/O 강연 게시물에서는 몇 가지 새로운 Fugu API를 살펴보고 Excalidraw PWA의 핵심 사용자 여정을 개선하는 방법을 살펴보겠습니다. 이렇게 하면 아이디어에서 영감을 얻어 여러분의 앱에 적용해 볼 수 있습니다.
내가 엑스칼리드로를 처음 접하게 된 계기
이야기로 시작하고 싶어요. 2020년 1월 1일, Facebook의 소프트웨어 엔지니어인 크리스토퍼 셰도는 자신이 개발을 시작한 작은 그림 앱에 관해 트윗했습니다. 이 도구를 사용하면 손으로 그린 것처럼 보이고 만화 같은 느낌의 상자와 화살표를 그릴 수 있습니다. 다음 날에는 타원과 텍스트를 그리고, 개체를 선택하여 이동할 수도 있습니다. 1월 3일에 앱의 이름이 Excalidraw로 결정되었으며, 모든 좋은 부업 프로젝트와 마찬가지로 크리스토퍼는 도메인 이름을 구매하는 것이 첫 번째 작업 중 하나였습니다. 이제 색상을 사용하고 그림 전체를 PNG로 내보낼 수 있습니다.
1월 15일 크리스토퍼는 저를 포함한 트위터에서 큰 관심을 받은 블로그 게시물을 게시했습니다. 이 게시물은 인상적인 통계로 시작했습니다.
- 순 활성 사용자 12,000명
- GitHub 별표 1,500개
- 참여자 26명
불과 2주 전에 시작한 프로젝트의 경우, 그리 나쁘지 않습니다. 그러나 저의 관심을 크게 고조시킨 것은 게시물 아래 끝부분에 있었습니다. 크리스토퍼는 이번에 새로운 시도를 했다고 말했습니다. 풀 리퀘스트를 제출한 모든 사용자에게 무조건적인 커밋 액세스 권한을 부여한 것입니다. 블로그 게시물을 읽은 날 바로 Excalidraw에 File System Access API 지원을 추가하여 누군가 제출한 기능 요청을 수정하는 풀 요청을 올렸습니다.
하루 후 내 풀 요청이 병합되었고 그때부터 전체 커밋 액세스 권한을 갖게 되었습니다. 물론 권력을 남용하지는 않았습니다. 지금까지 149명의 참여자 중 누구도 이 문제를 신고하지 않았습니다.
현재 Excalidraw는 정식 설치 가능한 프로그레시브 웹 앱으로, 오프라인 지원, 놀라운 다크 모드, 그리고 File System Access API를 통한 파일 열기 및 저장 기능을 제공합니다.
리피스가 Excalidraw에 많은 시간을 할애하는 이유
이제 'Excalidraw를 사용하게 된 계기'에 관한 이야기는 끝났습니다. Excalidraw의 멋진 기능을 살펴보기 전에 Panayiotis를 소개해 드리겠습니다. lipis로 알려진 인터넷의 Panayiotis Lipiridis는 Excalidraw에 가장 많이 기여한 기업입니다. 나는 립피스에게 그가 엑스칼리드로에 그렇게 많은 시간을 할애하게 된 동기가 무엇인지 물었습니다.
다른 사람들처럼 저도 크리스토퍼의 트윗을 통해 이 프로젝트에 대해 알게 되었습니다. 제가 가장 먼저 기여한 내용은 현재 Excalidraw에 포함된 색상인 Open Color 라이브러리를 추가하는 것이었습니다. 프로젝트가 커지고 요청이 많아짐에 따라, 다음으로 큰 기여는 사용자가 공유할 수 있도록 그림을 저장할 수 있는 백엔드를 구축하는 것이었습니다. 하지만 제가 진짜로 기여하게 된 이유는 누구든지 Excalidraw를 시도하여 그것을 다시 사용할 구실을 찾고 있다는 것입니다.
리피스님과 완전히 동의합니다. 누구든 엑스칼리드로를 다시 사용할 구실을 찾고 있습니다.
Excalidraw 사용하기
이제 Excalidraw를 실제로 사용하는 방법을 보여드리겠습니다. 제가 훌륭한 아티스트는 아니지만 Google I/O 로고는 간단하므로 한 번 해보겠습니다. 상자는 'i', 선은 슬래시, 'o'는 원입니다. Shift를 누르면 완벽한 원이 그려집니다. 잘 보이게 슬래시를 조금 이동하겠습니다. 이제 'i'와 'o'를 색칠해 보죠. 파란색이 좋습니다. 다른 채우기 스타일을 사용해 보세요. 전부 단색인가요, 아니면 크로스 해치? 아니, hachure는 멋지다. 완벽하지는 않지만 엑스칼리드로의 개념이므로 저장하겠습니다
저장 아이콘을 클릭하고 파일 저장 대화상자에 파일 이름을 입력합니다. 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를 지원하지 않는 브라우저를 위한 구현은 짧습니다. 원하는 파일 이름이 값이고 href
속성 값으로 blob URL을 사용하는 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
폴더의 파일 앱입니다. 두 개의 파일이 표시됩니다. 그중 하나는 설명할 수 없는 이름 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
가 포함된 단일 요소 배열을 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로 표시됩니다.
Google 간행물의 열렬한 독자라면 Project Fugu의 맥락에서 진행된 초기 단계 제안인 Idle Detection API를 통해 유휴 감지가 실현된다고 생각할 수 있습니다. 스포일러 주의: 그렇지 않습니다. Excalidraw에서 이 API를 기반으로 구현했지만 결국 포인터 이동과 페이지 가시성을 측정하는 보다 전통적인 접근 방식을 사용하기로 결정했습니다.
Idle Detection API로 기존 사용 사례를 해결하지 못한 이유에 관한 의견을 제출했습니다. 모든 Project Fugu API는 공개적으로 개발되고 있으므로 누구나 참여하여 의견을 제출할 수 있습니다.
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의 나머지 부분도 즐겁게 시청하세요.