원시 텍스트를 드라이브의 형식화된 Word Doc으로
Source: Dev.to
문제점
대부분의 메모 앱은 너무 복잡하거나 Word 문서를 제공하지 않는다. 나는 최소한의 기능만 원했다: 텍스트를 입력하고, 폴더를 선택하고, .docx 파일로 Drive에 저장한다. 간단해 보였지만 구현하면서 몇 가지 예상치 못한 어려움이 나타났다.
사용한 기술
- React와 Vite를 프론트엔드에 사용.
- docx.js(CDN 로드) – 브라우저에서 Word 문서를 생성 (npm 버전은 Node.js 전용).
- mammoth.js(CDN) – 기존
.docx파일을 읽어 “추가” 기능을 구현. - Google Identity Services와 Google Drive REST API v3 – 인증 및 Drive 접근.
Google Drive API 설정
1단계
Google Cloud Console에 들어가 새 프로젝트를 만든다.
2단계
Google Drive API를 활성화한다.
3단계
OAuth 동의 화면을 설정한다:
- 앱을 External 로 지정.
- 테스트 사용자로 자신의 이메일을 추가(403 오류 방지).
drive.file(업로드)와drive.readonly(폴더 목록) 스코프를 추가.
4단계
Credentials 아래에 OAuth 2.0 Client ID를 만든다:
- Application type: Web Application.
- Vite용 로컬 URL(
http://localhost:5173/)을 추가(포트 3000이 아님).
5단계
Client ID를 복사해 .env 파일에 VITE_GOOGLE_CLIENT_ID 변수로 저장한다.
앱 작동 방식
사용자가 로그인하면 앱은 Google Identity Services Token Client를 이용해 브라우저만으로 OAuth 액세스 토큰을 얻는다—서버 콜백이 필요 없다. 이후:
- Google
userinfo엔드포인트에서 사용자의 이름을 가져온다. - Drive API를 통해 모든 Drive 폴더를 불러온다.
사용자 흐름
- 기존 폴더를 드롭다운에서 선택 또는 새 폴더 이름을 입력해 즉시 생성한다.
- 폴더를 선택한 뒤 제목과 원시 텍스트를 입력하고 Save 버튼을 클릭한다.
핵심: Word 문서 생성
원시 텍스트는 docx.js에 전달되어 메모리 상에 올바른 Word 문서가 만들어진다. 구성 요소는:
- 헤딩
- 타임스탬프
- 각 텍스트 라인을 포맷된 단락으로 변환
중요:
Packer.toBlob()을 사용한다—브라우저에서는Packer.toBuffer()나Packer.toArrayBuffer()가 조용히 실패하거나 오류를 발생시킨다.
Google Drive에 업로드
생성된 Blob은 multipart upload 엔드포인트에 FormData로 전송한다:
- 파일 메타데이터(부모 폴더 ID 포함)를 담은 파트 하나
- 실제 파일 내용을 담은 파트 하나
기존 파일을 업데이트하려면 POST 대신 PATCH를 사용한다.
“기존 파일에 추가” 기능
- 선택한
.docx파일을 Drive에서 다운로드한다. - mammoth.js로 텍스트를 추출한다.
- 새 섹션 헤딩 아래에 기존 내용과 새 내용을 병합한다.
- 전체 문서를 다시 만든다.
- 원본 파일을 교체하도록 재업로드한다.
Netlify에 배포
npm run build실행.dist폴더를 Netlify에 드래그.- 사이트 설정에서
VITE_GOOGLE_CLIENT_ID를 환경 변수로 추가. - Google Cloud Console에서 OAuth 클라이언트의 Authorized JavaScript Origins에 Netlify URL을 등록한다.
참고: Google Cloud에서 앱이 Testing 모드일 경우 테스트 사용자로 추가된 이메일만 로그인할 수 있다. OAuth 동의 화면에서 Production으로 전환하면 일반 사용자도 접근 가능해진다.
내가 겪은 흔한 실수들
- Vite용 포트
5173대신3000을 사용. - 테스트 사용자를 추가하지 않아 403 오류 발생.
Packer.toBuffer()대신Packer.toBlob()을 사용하지 않음.- Netlify URL을 Authorized JavaScript Origins에 추가하지 않음.