ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Next.js 14] Google SpreadSheets를 이용한 다국어 지원 서비스 자동화 및 발생한 오류 해결 방법 정리 [i18next][Javascript]
    웹 개발/Nextjs 14 2024. 3. 20. 04:54

    얼마 전(?)에 작성한 App router 기반 Localization 기능을 자동화하기로 했다.

     

    [Next.js 14] App router 기반 Localization / Internationalization [i18next]

    다국어를 지원하는 웹에 들어가보면 드롭다운으로 언어를 바꾸고, 그에 맞게 텍스트가 휙휙 바뀌는 것을 본 적이 있을 것이다. 이번 포스팅에서는 i18next 모듈을 이용해서 앱 라우터 기반의 Next.j

    hotsunchip.tistory.com

     

    설명

    지금 자동화할 부분은 구글 스프레드시트에서 단어를 가져와서 각 나라별 json으로 변환하는 부분이다.

    이런 비슷한 로직을 유니티 uiToolkit 이용해서 ui 다국어 지원을 구현할 때 파이썬으로 작성했었다.

    간단하게 설명하자면 excel로 작성한 파일을 csv 형식으로 내보내 이를 json으로 변환하는 코드였는데,

    경험해본 결과 csv로 내보내고 파일 위치 옮기고 하는 과정 조차 귀찮았고, 조금이지만 사람 손을 거쳐야 해서 실수가 발생할 수도 있었다.

    그래서 이번에는 csv로 내보낼 필요 없이 바로 구글 스프레드시트를 읽어올 수 있도록 했다.

     

     

    여러 옵션을 생각하다가 Google SpreadSheets를 사용한 이유는 아래의 두 가지이다.

    1. 구글 스프레드시트의 자동 번역(GOOGLETRANSLATE)을 사용할 수 있다.
    2. 보통 번역은 비개발자 분들이 해주시는데, 이분들이 작업하기 가장 편하고 익숙한 툴이다.

    그럼 바로 가쟈~

     

    구글 스프레드시트 생성

    먼저 번역 단어를 저장할 스프레드시트를 생성해준다.

    자동 변역 함수(GOOGLETRANSLATE) 사용

    사실 구글 번역을 100% 믿지도 않고, 도메인 관련한 내용이나 해당 업계에서만 사용되는 용어, 그리고 다의어 등은 다시 작업이 필요하다.

    그래도 본격적인 검수 전 테스트용이나 기본 틀 작성용으로는 제격이다. 자세한 사용법은 아래와 같다.

     

    나는 한국어 기준으로 번역을 돌렸고, 번역 텍스트가 없어 에러가 날 경우를 대비하여 IFERROR을 이용해 에러 핸들링을 해주었다. 아래 사진의 수식을 참고하여 작성해보자.

    스프레드시트 구성

    추가로 나는 가장 첫 열을 localization key, 첫 행을 country code로 사용하였다.

    나중에 나올 자바스크립트도 이를 기준으로 작성되어 있으니, 염두에 두면서 작업을 진행하면 될 것 같다.

    맨 아래에 보이는 것처럼 시트를 분리하여, 작성하여 관리를 더 쉽게 하고 i18next의 multi-namespace 기능을 이용하기 위해

    json으로 내보낼 때 각 시트 이름을 파일명으로 가지는 json 파일을 생성할 예정이다.

     

    Google Sheets API 사용하여 동기화

    Google Cloud Console에서 1~3번의 과정을 진행한다.

     

    1. 새 프로젝트 생성 (이미 생성되어 있는 경우 생략 가능)

     

    2. Google Sheets API 사용 설정

     

    좌측 메뉴에 API 및 서비스 > 라이브러리 메뉴로 이동하여 Google Sheets API를 검색한 후, 사용 버튼을 눌러 활성화해준다.

     

    3. API 키 생성

    같은 메뉴에서 사용자 인증 정보 탭으로 간 후, 사용자 인정 정보 만들기 > API 키 버튼을 눌러 새로운 API 키를 생성해준다.

    그럼 API 키가 뜨는데, 이를 복사해 메모장 같은 곳에 옮겨놓자.

     

    키가 생성된 후에는 오른쪽 아이콘 > API 키 수정 메뉴를 눌러 API 제한 사항을 추가해준다.

     

    4. .env 파일에 키 저장

    이제 방금 만든 API 키와 번역본이 있는 spreadsheet ID를 .env 파일에 추가해준다.

    만약 .env 파일이 없다면, 프로젝트 루트 경로에 .env 파일을 만들고 .gitignore 파일에 추가해주자.

    구글 시트의 id는 해당 시트의 url에서 확인할 수 있는데,

    https://docs.google.com/spreadsheets/d/{googleSheetID}/솰라솰라 의 형식이라 찾기 쉬울 것이다.

    API 키는 아까 옮겨둔 애를 다시 복사해서 붙여주거나, 새로 복사해서 붙여준다.

    // .env
    SHEET_ID=구글시트아이디
    GOOGLE_SHEETS_API_KEY=복사해둔API키

     

    Google Sheets -> json 변환 코드 작성

    나중에 설명할 버그 해결이 모두 적용된 버전이다.

    typescript 말고 javascript로 작성한 이유는, ts의 경우 js로 변환하는 과정이 한번 더 필요해서이다.

    어차피 이 파일은 빌드 전 콘솔에서 실행할 스크립트라 굳이 ts로 작성하는 것보다 js로 작성하는 것이 깔끔하다고 판단했다.

    google-spreadsheetdotenv가 설치되어 있지 않다면 npm i 명령어를 이용해 설치해주자.

    dotenv는 환경변수를 사용하기 위해 필요하다.

    // src/utils/localization/downloadLocales.js
    const fs = require('fs');
    const googleSpreadsheet = require('google-spreadsheet');
    const dotenv = require('dotenv');
    
    dotenv.config();
    
    (async function makeJson() {
      if (!process.env.SHEET_ID || !process.env.GOOGLE_SHEETS_API_KEY) return;
      
      const doc = new googleSpreadsheet.GoogleSpreadsheet(process.env.SHEET_ID, {
        apiKey: process.env.GOOGLE_SHEETS_API_KEY,
      });
    
      await doc.loadInfo();
    
      const sheets = doc.sheetCount;
    
      for (let i = 0; i < sheets; i++) {
        const sheet = doc.sheetsByIndex[i];
        await sheet.loadCells();
        const rows = await sheet.getRows();
        const langs = sheet.headerValues;
    
        if (langs.length === 4) {
          langs.shift();
        }
        else if (langs.length === 0) {
          return;
        }
    
        const jsonData = {};
    
        langs.forEach((language, index) => {
          for (let j = 1; j < rows.length + 1; j++) {
            jsonData[sheet.getCell(j, 0).value] = sheet.getCell(j, index + 1).value;
          }
    
          const jsonString = JSON.stringify(jsonData, null, 2);
    
          // JSON 파일 생성
          fs.writeFileSync(
            `src/utils/localization/locales/${language}/${sheet.title}.json`,
            jsonString
          );
        });
      }
    })();

     

    빌드 시 자동화

    package.json의 "scripts" - "build" 커맨드를 수정하여 완료했다.

     

    버그 해결

    이제부터는 자동화 과정에서 발생한 버그를 위주로 정리하려고 한다.

    module 오류 (Node.js cannot use import statement outside a module)

    처음에는 typescript로 작성해서 앞의 모듈 불러오는 부분을 아래와 같이 작성했다.

    import fs from 'fs';
    import { GoogleSpreadsheet } from 'google-spreadsheet';
    import dotenv from 'dotenv';

     

    이러고 node src/utils/localization/downloadLocales.js 명령어를 이용해 실행하니까,

    module 밖에서는 import를 사용할 수 없다는 에러(Node.js cannot use import statement outside a module)가 발생했다.

     

    검색해보니 대부분 해결책으로 package.json의 아래에 해당 구문을 추가하는 방법을 소개해주고 있었다.

    {
      // ...
      "type": "module",
      // ...
    }

     

    나 또한 이 방법으로 문제가 해결되는 것으로 보였으나, require을 사용하는 외부 라이브러리에서 문제가 우후죽순 발생하였다.

    그래서 위 구문을 추가하는대신 import로 require로 바꾸는 작업을 진행하였다.

    const fs = require('fs');
    const googleSpreadsheet = require('google-spreadsheet');
    const dotenv = require('dotenv');

     

    permission 오류 (The caller does not have permission)

    module 에러를 해결하고 나니 스크립트가 제대로 실행됐다. 그런데...

    {
      "error": {
        "code": 403,
        "message": "The caller does not have permission",
        "status": "PERMISSION_DENIED"
      }
    }

     

    에러가 떴다. 권한이 없댄다.

    API 키를 잘못 설정했나? 싶었는데, 애초에 읽기 권한만 있으면 돼서 앞서 언급한 과정 외에 추가적인 과정은 필요 없다는 건 확실했다.

    그래서 혹시나 해서 구글 시트의 공유 옵션을 '링크가 있는 모든 사용자'로 변경하니 잘 작동했다.

    이게 정말 해결 방법이 맞나 싶긴 한데, localization된 단어는 크게 보안이랑 상관 없고,

    또 시트 id를 .env 파일에 보관하니 괜찮을 것 같아 이대로 진행시켰다.

     

    시트의 마지막 열 누락 (googlesheets getRows lost last row)

    논리적으로 생각한 로직으로 스크립트를 작성했을 때, 각 시트의 마지막 열이 누락되는 것을 확인했다.

    간단하게 rows.length + 1만큼 iteration을 돌려 해결했다.(downloadLocales.js의 35번째 줄 참고)

     

     

     

    지금 AWS Amplifier 이용해서 main 브랜치에 머지되면 자동으로 build하도록 자동화가 되어 있어

    머지 전에 로컬에서 빌드 테스트를 진행하는데, 빌드 명령어에 localization 동기화 하는 명령어를 추가하니까 짱 편하다.

    이래서 자동화하는구나 싶다. 코딩에만 집중할 수 있고 나중에 인수인계하기도 편할듯!

     

    끗~