계기

애써서 만든 i18n json 파일을 좀더 효율적으로 쓰고 싶었다.

원래도 번역자(스프레드시트) <-> 개발자(json)으로 소통하고 있었는데,

번역자가 여러명이라 싱크도 맞추기 힘들고, 개발자 입장에서는 코드에 한번 시트에 한번 계속 입력해주는게 귀찮았기 때문에

한 줄로 해결할 수 있는 방법을 찾고 싶었다.

그래서 Google spreadsheet API를 활용해서 json을 -> 엑셀로, 엑셀을 -> json으로 옮기는 툴을 만들었다,

i18n-scanner 라는 라이브러리도 있었는데 그냥 json만 불러오는게 빠를 것 같아서 안 썼다.

 

준비물

구글 서비스 계정 생성

https://console.cloud.google.com/apis/credentials 에 가서

1. 프로젝트 생성하고 서비스 계정을 생성하고 키에 가서 JSON 형태의 비공개 키를 만들어 다운받는다.

2. API 및 서비스 > 라이브러리에 가서 Google Sheets API를 사용한다.

3. 제어하고 싶은 구글 스프레드시트 파일에서 json 파일에 있는 client_email 주소를 공유한다.

위 과정의 결과물로 JSON 파일, 스프레드시트 Document ID, sheet ID를 얻었으면 끝.

 

패키지 설치

React & typescript 환경에서 코드 내부에서 버튼 하나 누르면 동기화되는 그런 시스템을 생각했는데

안타깝게도 그렇게 구축하는 것과 그냥 node XXX.js 를 해버리는 것의 큰 차이가 없었다. (이유는 후술)

따라서 react 프로젝트 안에서 돌아가는 js를 만들었다. 프로젝트는 그냥 package.json을 위해 희생당했다고 보면 된다.

이런저런 많은 패키지를 테스트해보긴 했는데 결국 필요한건 하나였다.

yarn add -D googleapis

https://www.npmjs.com/package/googleapis 하나만 설치해주자.

 

연동 테스트

const { google } = require('googleapis');
require('dotenv').config();

// 인증 정보
const client = new google.auth.JWT(
    process.env.GOOGLE_CLIENT_EMAIL,
    null,
    process.env.GOOGLE_PRIVATE_KEY,
    ['https://www.googleapis.com/auth/spreadsheets'] // Google Sheets API 권한
);

// Google Sheets API 호출 함수
async function getSheetData() {
  try {
    const googleSheets = google.sheets({ version: 'v4', auth: client });

    // 스프레드시트에서 데이터를 읽어옴
    const response = await googleSheets.spreadsheets.values.batchGet({
      spreadsheetId: DOCID, // 스프레드시트 ID
      ranges: sheets, // 읽고자 하는 데이터 범위
    });

    const sheetData = response.data.valueRanges.map((valueRange, index) => {
      return {
        sheet: sheets[index],
        data: valueRange.values,
      };
    });

    return sheetData;
  } catch (error) {
    console.error('Error accessing Google Sheets API:', error);
  }
}

// 함수 실행
getSheetData().then((data)=>{
	console.log(data);
});

 

문제의 new google.auth.JWT가, 브라우저에서는 동작을 안 하는 바람에 반나절정도 삽질했다. 보안 상의 이유로 서버에서만 동작한다고 한다. 자료도 없고, 블로그들 코드가 죄다 안되어서 고생을 좀 했다. 오늘 기준으로 잘 동작된다.

 

파일 경로+이름이 {path}/a.js 라면, node {path}/a.js 하면 된다. 참 쉽죠?

 

이 코드만 적용 성공했다면, 이제 읽어오는 것은 두려움 없이 할 수 있다.

맨 아래 실행 함수의 .then 부분에 파일을 저장하는 로직을 넣으면 된다. 이건 찾아보면 많이 나오니까 직접 해보자. 쉽다. (fs.writeFileSync 를 썼다)

 

process.env의 두 값은 JSON에서 CLIENT_EMAIL과 PRIVATE_KEY env에 적용한 값이다.

env를 잘 쓰려면 require('dotenv').config(); 를 꼭 import 해줘야 한다. (.env 파일을 만드는 것도 포함하여)

 

참고로 batchGet은 여러 시트를 불러올 때 사용하는 것으로, 그냥 get으로 사용할 수도 있다. update도 마찬가지.

테스트를 통해 값 읽어오기는 했으니 이제 시트에 업데이트도 해보자.

 

시트에 업데이트 하기

async function updateGoogleSheet() {

  try {
    const googleSheets = google.sheets({ version: 'v4', auth: client });

    // 스프레드시트에 데이터를 추가
    const response = await googleSheets.spreadsheets.values.batchUpdate({
      spreadsheetId: DOCID, // 스프레드시트 ID
      valueInputOption: 'RAW', // 데이터를 입력할 방식 RAW || USER_ENTERED
      resource: {
        data: [
          {
            majorDimension: 'ROWS', // 행 단위로 데이터 처리
            range: 'test!A1', // 데이터를 추가할 위치
            values: **YOUR DATA**,
          },
          ...
        ],
      },
    });

    console.log(`${response.data} cells updated.`);
  } catch (error) {
    console.error('Error updating Google Sheets API:', error);
  }
}

// 함수 실행
updateGoogleSheet();

 

여기는 사실상 batchUpdate 함수가 어떻게 쓰이는지만 알면 금방 할 수 있을 것이라고 생각한다.

  • valueInputOption은 입력된 데이터를 그대로 쓸지, 엑셀에 사용자가 입력할 때처럼 자동변환 해줄지 하는 옵션이다.
  • majorDimension은 데이터 처리 방식인데, ROWS냐 COLUMNS냐를 고를 수 있다. 2차원 배열 읽는 순서다.
    • [[1, 2, 3], [4, 5, 6]] 를
    • ROWS 로 넣으면 1 2 3 / 4 5 6이 되고
    • COLUMNS 로 넣으면 1 4 / 2 5 / 3 6이 된다.
  • range는 {시트이름}!{범위} 식의 포맷으로 엑셀 범위 지정할때랑 똑같이 쓰면 된다.
  • values에는 위에서 예시를 들었던 [[1, 2, 3], [4, 5, 6]] 처럼 2차원 배열을 넣으면 된다.

마찬가지로 여기서는 readdirSync 를 통해서 json(locale)파일을 읽어왔다. (코드에서는 생략했다.)

 

 

후기

자료가 정말정말 없었다. 왜 다른 사람들은 다 잘 되었을까?

지금 생각해보면... 말도 안되는 짓(주로 JWT)을 하고 있었기 때문이 아닐까...

내가 만드는 코드가, 어디서 돌아가고 있는지

어느 프레임워크를 쓰고 있는지, 어떤 방식으로 빌드되는지 아는 것은 중요한 것이라고 생각한다.

환경 변수에 대해서도 조금 더 알게 되었다.

누군가가 이 글을 보고 고통에서 해방되길...

 

 

참고한 블로그

https://ui.toast.com/weekly-pick/ko_20210303

https://soojae.tistory.com/67

https://uni-s-code.tistory.com/42

https://www.npmjs.com/package/google-auth-library

https://www.npmjs.com/package/google-spreadsheet

'[Frontend] > React, TS, JS' 카테고리의 다른 글

[React & Typescript] i18n 적용기  (0) 2024.10.20

 

0. 계기

기존 프로젝트에서 다중 언어(3개 이상)를 제공하고 있었는데,
단순히 json 만 parsing하는 방식을 사용해서 언어 관리가 쉽지 않았다
(미번역 단어를 찾기나, 언어별로 missing key를 찾는 등의 일이 어려웠다.)
이에 i18n Ally와 같은 extension을 사용해 언어를 관리하고자 멀고도 험난한 여정을 거친 적용기다.

교훈: 처음 전략을 잘 세우자 (프로젝트 덩치가 조금 커서 거의 2주일을 소모했다.)

 

기존 json 정리 -> i18n 도입(초기 설정 -> i18n Ally -> 파일에 적용)의 순서를 거쳤다.

1. 기존 json 정리 (일주일+ 소모)

  • 기존 json이 약 7개 이상의 파일로 구성되어있었기에 이를 하나로 합체하였다.
  • 화이트라벨링 관련 표현도 따로 파일이 존재했는데, 이것은 i18n의 별도 네임스페이스로 두기 위해 남겼다.
  • 중복 표현&key를 제거하며 일부 정리를 거쳤다.
  • 다른 검토가 필요한 표현은 타 부서와의 협의를 위해 우선 두었다.

2. i18n 도입 (일주일 소모)

  • 초기 설정
    • 패키지 설치
    • 루트 또는 /src에 i18n.ts 파일을 만든다.
yarn add i18next i18next i18next-browser-languagedetector i18next-http-backend
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';

// i18n 초기 설정
i18n
  .use(Backend) // 서버에서 언어 파일을 가져오는 기능
  .use(LanguageDetector) // 사용자의 브라우저 언어를 감지하는 기능
  .use(initReactI18next) // react-i18next 연결
  .init({
    fallbackLng: 'en', // 기본 언어 설정
    returnEmptyString: false, // json에서 value가 "" 형태인 항목도 fallback 처리하도록 하는 플래그
    debug: true, // 개발 중 디버그 모드 활성화
    interpolation: {
      escapeValue: false, // 리액트는 기본적으로 XSS 공격을 방지하므로 별도 이스케이프가 필요 없음
    },
    ns: [**Your Namespaces**],
    defaultNS: '**Your Default Namespace**',
    backend: {
      loadPath: '**Your Path of Locale Files**/{{lng}}/{{ns}}.json', // 언어 리소스 파일 경로
    },
  });

export default i18n;

 

  • i18n Ally 설정
    • extension을 위해, `.vscode/settings.json` 에 아래와 같이 설정해준다.
"i18n-ally.localesPaths": ["Your Path of Locale Files"],
"i18n-ally.enabledFrameworks": ["react-i18next"],
"i18n-ally.keystyle": "nested"
// if using namespace...
"i18n-ally.defaultNamespace": "Your Default Namespace",
"i18n-ally.namespace": true,

 

  • 실제 사용: 함수형 컴포넌트
    • useTranslation hook을 사용하여 함수형 컴포넌트 내부에서 아래와 같이 사용할 수 있다.
import { useTranslation } from 'react-i18next';

...
// default namespace만 사용할 경우
const { t, i18n } = useTranslation();
// namespace를 더 사용할 경우
const { t, i18n } = useTranslation([a, b, ...]);

console.log(t('test')) // default namespace는 생략 가능
console.log(t('a:test'))
console.log(i18n.language) // 현재 사용중인 언어
...

 

  • 실제 사용: 파일
    • 아래처럼 그냥 import해서 쓸 수도 있다. (권장하진 않는다.)
import { t } from 'i18next';
import i18n from 'src/i18n';
import type { WithTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';

class Test extends React.PureComponent<PropsType & WithTranslation, StateTypes> {
	constructor(props: PropsType & WithTranslation) {
    	super(props);
    	...

    render() {
        const { t } = this.props;
        return <p>{t('test')}</p>;
	}
 }

export default withTranslation()(Test);

// 뭔가로 이미 감싸고 있었다면
export default connect(...)(withTranslation()(Test));

// forwardRef를 써야한다면
const TestWithRef = React.forwardRef((props: WithTranslation, ref: React.Ref<Test>) => {
  return <Test {...props} ref={ref} />;
});

export default withTranslation()(TestWithRef);

3. 결론

  • 언어 설정은 초반에 잘하자.
  • 화이트라벨링을 더 잘 적용할 수 있는 방법이 있으면 좋겠다.
  • namespace를 좀더 명확하게 가져올 수 있는 방식이 있었으면 좋겠다. : 으로 구분하는 방식이 보기 어려운 것 같다.(어처피 ally 쓸거지만)

 

다음은 이렇게 변환한 i18n json을 가지고 구글 스프레드시트에 연동하는 tool을 만든 후기를 작성하겠다.

 

** 여담

Uncaught Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.  에러가 발생한다면

<Suspense> 로 감싸져있는지 확인하자.

이유는 여기 블로그에 잘 나와있다.

https://velog.io/@leejungho9/Error-A-component-suspended-while-responding-to-synchronous-input.-This-will-cause-the-UI-to-be-replaced-with-a-loading-indicator.-To-fix-updates-that-suspend-should-be-wrapped-with-startTransition

 

[Error] A component suspended while responding to synchronous input.

최근 리액트로 프로젝트를 진행하다 다음과 같은 에러를 마주했다. A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading

velog.io

 

** 여담2

locale 파일을 public에 넣지 않으면 실제 서비스 할 때 정보를 못 불러오는 문제가 있었다.

원인은 아직 파악 못했지만 public에 넣어야 하는 듯 하다.

'[Frontend] > React, TS, JS' 카테고리의 다른 글

[node.js] Google Spreadsheet API 사용기  (3) 2024.10.25

+ Recent posts