Context API 와 Recoil

 

이제 1년차인 응애 개발자여서 처음에는 랜더링을 크게 신경쓰지 않고 우선 기능에 초점을 맞춰 개발해나갔다.

하지만 애플리케이션이 점점 커지고 child component들이 많아지던 어느 순간부터 '어? 랜더링 최적화 해야겠는데..?' 라고 생각 될 정도로 성능이 지수함수 형태로 안좋아지기 시작했다.

 

랜더링 최적화의 방법은 React.memo, useMemo, useCallback, 상태와 props의 좋은 아키텍쳐 설계, 상태관리 라이브러리 사용, key값 사용(diffing 알고리즘) 등등 많지만 여기서는 contextAPI와 recoil을 통해 해결했던 방법을 서술하겠음!

 

전역 상태관리를 라이브러리로 recoil을 사용했는데, 당시엔 개발할 애플리케이션 규모가 작아 복잡한 상태관리가 없을거라 판단해 직관적인 recoil을 선택했다. recoil 상태와 setter 함수를 가져와 커스텀훅도 만들고 열심히 개발할때는 몰랐는데 성능 최적화에 손을 대는 순간 내가 react도 recoil도 잘못 사용하고 있었다는것을...  깨달았다.

 

react는 상태가 바뀌면 리랜더링이되고, recoil은 기본적으로 setter 함수와 상태를 구분해서 불러올 수 있다.

아래 예시를 보자.

// atom 생성
export const numberState = atom({
  key: "numberState",
  default: 0,
});


// Test 컴포넌트
const Test = () => {
  const number = useRecoilValue(numberState); // numberState atom에서 value 값만 사용 
  console.log('number: ', number);

  return (
    <div>
      <h1>{number}</h1>
    </div>
  );
}


// Test2 컴포넌트
const Test2 = () => {
  const setNumber = useSetRecoilState(numberState); // numberState atom에서 setter함수만 사용
  const addNumberHandler = () => {
    setNumber((prev) => prev + 1);
  }
  console.log('Test2 리렌더링');

  return (
    <div>
      <button onClick={addNumberHandler}>
        <h1>test2</h1>
      </button>
    </div>
  );
}

 

 

아래 화면을 보면 같은 key의 atom에서 가져왔지만

Test2의 setter 함수를 실행시키면 state를 가진 Test 컴포넌트만 리랜더링 된다.

Test 컴포넌트만 리랜더링 된 모습

 

하지만 이걸 커스텀 훅으로 만들면?

// 커스텀훅 생성
const useRecoilCustomhook = () => {
  const [number, setNumber] = useRecoilState(numberState);

  const addNumberHandler = () => {
    setNumber((prev) => prev + 1);
  };

  return {
    number,
    addNumberHandler,
  };
};

export default useRecoilCustomhook;

// Test 컴포넌트
const Test = () => {
  const { number } = useRecoilCustomhook(); // 커스텀훅에서 state 사용
  console.log('number: ', number);

  return (
    <div>
      <h1>{number}</h1>
    </div>
  );
}

// Test2 컴포넌트
const Test2 = () => {
  const { addNumberHandler } = useRecoilCustomhook(); // 커스텀훅에서 함수 사용
  console.log('Test2 리렌더링');

  return (
    <div>
      <button onClick={addNumberHandler}>
        <h1>Test2</h1>
      </button>
    </div>
  );
}

 

당연하게 Test2 컴포넌트도 함께 랜더링된다.

addNumberHanlder 가 실행되면 커스텀훅에 있는 number state가 랜더링되며 커스텀훅을 사용하는 모든 컴포넌트에서 랜더링이 발생하는 당연한 현상이 일어납니다. (지금은 당연하지만 그때는 아니었습니다.. 또륵)

커스텀훅을 사용하는 모든 컴포넌트에서 랜더링 발생

 

물론 위 예시는 간단하니까 금방 최적화 할수 있지만 당시 커스텀훅 남발로 인해 리팩토링이 시급했고..

꼬인 코드를 떼어낼 방법으로 contextAPI 로 state 관심사를 분리해줬다.

그리고 context에서 상태와 setter 함수를 분리해서 상태를 가진 컴포넌트가 아니면 리랜더링이 되지않도록 했다.

관련 내용 docs

export const StateContext = createContext(); // 상태context 
export const SetterContext = createContext(); // setter 함수 관련 context

const GlobalProvider = ({ children }) => {

  const [number, setNumber] = useState(0);
  const addNumberHandler = () => {
    setNumber(prev => prev + 1);
  };

  const stateValue = useMemo(() => ({
    number
  }), [number]);

  const setterValue = useMemo(() => ({
    addNumberHandler
  }), []);

  return (
    <StateContext.Provider value={stateValue}>
      <SetterContext.Provider value={setterValue}>
        {children}
      </SetterContext.Provider>
    </StateContext.Provider>
  );
}

export default GlobalProvider;

 

아래와 같이 SetterContext를 사용한 컴포넌트는 StateContext 와 달리 랜더링 되지 않는다

const Test = () => {
  const { number } = useContext(StateContext); // StateContext 구독
  console.log('number: ', number);

  return (
    <div>
      <h1>{number}</h1>
    </div>
  );
}

const Test2 = () => {
  const { addNumberHandler } = useContext(SetterContext); // SetterContext 구독
  console.log('Test2 리렌더링');

  return (
    <div>
      <button onClick={addNumberHandler}>
        <h1>Test2</h1>
      </button>
    </div>
  );
}

  

state 와 setter context를 별도로 나눠 state쪽 context를 구독한 컴포넌트만 랜더링된 모습

 

위 context 분리 방법의 이점은 전역상태와 setter 함수를 별도로 관리하니 코드 관리와 유지보수가 용이한 점과 전역으로 사용할 state와 컴포넌트를 캡슐화 시킬수 있는 점이었는데, 위에서 특정 recoil state와 setter 함수로 커스텀훅을 사용하듯 전역으로 사용할 state와 settter 함수를 특정 컴포넌트들과 매칭시켜 코드를 큰 덩어리끼리 분리할수 있었다.

 

그리고 recoil은 goole-maps-api의 map 객체와 같이 복잡한 객체를 state에 저장할 수가 없다. 처음엔 필요한 부분만 뽑아서 state에 부분저장 하거나 ref로 어떻게 어떻게 처리하다가 만들던 애플리케이션이 maps의 꽤 많은 기능들을 사용하다보니 처음엔 다른 상태관리 라이브러리를 사용할까 하다가 contextAPI 가 react에 내장된 기능이기도하고 다른 라이브러리를 사용하기보다 이쪽으로 해결해보고 싶었다.

 

Recoil Selector

 

recoil로 남발해 놓은 커스텀훅을 덜어내려고 고민하다보니 기존에 외면해왔던 기능들이 눈에 들어왔다.

몇번 docs 읽어보긴했었는데 음..음.. 그렇군 왜 사용하는지 모르겠군! 난 커스텀 훅을 사용하겠어! 왜냐하면 react는 커스텀훅을 권장하니까! 라는 근거도 없는 논리로 사용하지 않았었는데 다시보니 선녀였다.

 

contextAPI로 큰 덩어리끼리 컴포넌트와 상태를 어느정도 분리시켰지만 찐 전역으로 사용하는 상태는 여전히 많았고 해당 상태들은 여전히 recoil로 다뤄야했다.

selector 는 리팩토링 중에 상태들의 분리와 응집도를 높여주는데 많은 도움을 줬다.

이번엔 조금 바뀐 예제다

export const itemState = atom({ // 과일이 3개 있는 배열이다
  key: "itemState",
  default: [
    {
      id: 1,
      name: "사과",
      price: 1000,
    },
    {
      id: 2,
      name: "바나나",
      price: 2000,
    },
    {
      id: 3,
      name: "물렁한 복숭아",
      price: 3000,
    },
  ],
});

export const filterState = atom({ // filter 기능으로 사용할 상태
  key: "filterState",
  default: "all",
});


// selector는 다른 상태를 가져와 가공할 수 있게하는 순수함수(view만 제공)
export const filteredItemState = selector({ 
  key: "filteredItemState",
  get: ({ get }) => {
    const items = get(itemState); // 과일 상태 get
    const filter = get(filterState); // filter 상태 get

    switch (filter) { // filter 상태에 따라 과일 상태 가공
      case "cheap":
        return items.filter((item) => item.price <= 1000);
      case "normal":
        return items.filter((item) => item.price > 1000 && item.price <= 2000);
      case "expensive":
        return items.filter((item) => item.price > 2000);
      default:
        return items;
    }
  },
});
const Test = () => {
  // 원본 itemState 가 아닌 가공된 filteredItemState
  const items = useRecoilValue(filteredItemState);

  return (
    <div>
      {items?.map(item =>
        <ul key={item.id}>
          <li>상품명: {item.name}</li>
          <li>가격: {item.price}</li>
        </ul>
      	)
      }
    </div>
  );
}

const Test3 = () => {
  const setFilter = useSetRecoilState(filterState);
  const onChangeFilter = (e) => {
    const value = e.target.value;
    setFilter(value);
  }

  return (
    <div>
      <select defaultValue={"all"} onChange={onChangeFilter}>
        <option value={"all"}>all</option>
        <option value={"cheap"}>cheap</option>
        <option value={"normal"}>normal</option>
        <option value={"expensive"}>expensive</option>
      </select>
    </div>
  )
}

selector로 관련 state와 로직을 분리하고 모아뒀다 (유지보수 올라감)

 

원래는 Test 컴포넌트 안에 있어야할 필터 로직들이 분리되었고 상태도 관련있는 상태끼리 모아서 관리하는 이점이있다.

아래는 필터 로직이 컴포넌트에 있는 코드

const Test = () => {
  const items = useRecoilValue(itemState); // 원본 과일 상태 get
  const filter = useRecoilValue(filterState); // filter 상태 get

  // 필터링 로직
  const filteredItems = items.filter(item => {
    switch (filter) {
      case "cheap":
        return items.filter((item) => item.price <= 1000);
      case "normal":
        return items.filter((item) => item.price > 1000 && item.price <= 2000);
      case "expensive":
        return items.filter((item) => item.price > 2000);
      default:
        return items;
    }
  });

  return (
    <div>
      {filteredItems.map(item => (
        <ul key={item.id}>
          <li>상품명: {item.name}</li>
          <li>가격: {item.price}</li>
        </ul>
      ))}
    </div>
  );
}

 

 recoil 함께해서 즐거웠고 다신 보지말자~ 

 

뭐 여차저차 리팩토링에 도움은 많이 되었는데 recoil이 알아볼수록 관련 괴담이 많았다.

일단 selectorFamily, family는 selector나 atom 사용 시에 동적으로 파라미터를 전달해 상태를 사용할 수 있게하는 기능이다. 아래처럼 사용하면 된다.

const itemState = atom({ // 기존의 과일 상태
  key: 'itemState',
  default: [
    { id: 1, name: '사과', price: 1000 },
    { id: 2, name: '바나나', price: 2000 },
    { id: 3, name: '복숭아', price: 3000 },
  ],
});

// Family Selector 정의
const itemByIdState = selectorFamily({
  key: 'itemByIdState',
  get: (id) => ({ get }) => { // id 값에 따라 해당 값만 return
    const items = get(itemState);
    return items.find(item => item.id === id);
  },
});

// 컴포넌트에서 사용
function ItemDetail({ itemId }) {
  const item = useRecoilValue(itemByIdState(itemId));
  return (
    <div>
      <h3>상품명: {item?.name}</h3>
      <p>가격: {item?.price} 원</p>
    </div>
  );
}

 

But, selector 나 family 나 기본적으로 캐싱을 해주는데 매개변수가 변경되어 기존 값을 사용하지 않아도 가비지 컬렉터에 제대로 수집되지 않아 해당 데이터가 메모리에 남아있어 메모리 누수가 발생한다.

아직도 열려있는 21년도 gitissue..

현재 기준 최신 버전 업그레이드도 1년전이고 무엇보다 활발한 커뮤니티가 있는 유사한 atom 개념인 jotai 도 있고 곰돌이 zustand도 있고 앞으로는 recoil을 프로젝트에 적용하는 일은 없을 것 같다.

 

그래도 찍먹해봤으니 만족~

 

참고 글

recoil Git_issue

Recoil, 이제는 떠나 보낼 시간이다

https://tech.osci.kr/recoil-selector/

i18next react에서 사용하기

 

1탄에서 열심히 json 파일 자동화 시켰으니까 이제 사용하는 프로젝트에 연동하면 됩니다.

기본 골자는 아래와 같습니다.

 

1. i18n.js 파일 만들기

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
// LanguageDetector 는 다양한 방법으로 사용자 pc에서 기본 언어를 감지해 설정한다
// 자세한 내용은 아래에 후술하겠음

import translationEN from "../locale/en/en.json";
import translationKO from "../locale/ko/ko.json";
// 위처럼 그냥 json 가져와도 잘 읽힌다

const resources = {
  en: {
    translation: translationEN,
  },
  ko: {
    translation: translationKO,
  },
};

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: "ko", // 번역 파일에서 찾을 수 없는 경우 기본 언어
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

 

2.최상위 파일에서 i18n.js 빌드하기

import React from 'react'
import App from './App.jsx'
import "./locale/i18n.js" // 최상위 파일에서 build 해준다

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

 

3. 다국어 지원을 할 컴포넌트에서 사용

import { useTranslation } from "react-i18next";

export default function I18nTest() {
  const { t, i18n } = useTranslation();

  // 언어 변경하기
  const changeLanguage = (lang) => {
    i18n.changeLanguage(lang); 
  };

  return (
    <div>
      <div>
        <button onClick={() => changeLanguage("ko")}>한국어</button>
        <button onClick={() => changeLanguage("en")}>English</button>
      </div>
      <h1>{t(`auto-translation`)}</h1> // json 파일에 설정했던 key 값으로 불러온다
      <div>{t(`new-jeans`)}</div> // json 파일에 설정했던 key 값으로 불러온다
    </div>
  )
}

 

이번에 조금 더 딥하게 공부하면서 i18next의 디테일에 놀란 부분이 몇개 있다

 

첫번째는 i18next-browser-languagedetector 의 기능

무려 5가지 방법을 통해 사용자 언어를 감지해준다 그리고 i18n.changeLanguage() 로 언어를 변경하면 마지막으로 변경한

언어를 기억해서 다시 접속하면 해당 언어를 default로 보여준다. 

(electron 으로 개발할 때는 브라우저랑 좀 달라서 그런지 마지막 변경 기억을 안해줘서 따로 코드로 구현했었음..)

 

1. 쿼리 문자열

 - URL의 쿼리 문자열에서 언어를 감지. 예를 들어, ?lng=en과 같은 형식으로 언어를 지정.

2. 로컬 스토리지

 - 로컬 스토리지에서 언어를 감지. 쿠키와 유사하게 사용자 언어 설정을 저장하고 나중에 불러오는데 사용.

3. 쿠키

 - 이전에 설정된 언어를 쿠키에서 감지. 사용자가 이전에 선택한 언어 설정을 기억

4. 세선 스토리지

 - 세선 스토리지에서 언어 감지

5. 브라우저 설정

 - 브라우저 navigator.language 또는 navigator.languages 속성을 통해 사용자의 기본 언어를 감지.

 

나의 경우는 i18n.changeLanguage() 메서드로 언어를 변경할때마다 localStorage에 저장되고 있었다.

 

두번째는 useTranslation 훅

언어가 변경될 때마다 useTranslation 훅을 가진 컴포넌트를 리랜더링 해주는게 신기해서 안을 좀 까봤다.

useTranslation 안에 아래 useState로 상태관리를 해주고 있었고
const [t, setT] = react.useState(getT);

ns(언어) 가 변경되는걸 트리거로 useEffect 에서 setter 함수로 state 변경해서 랜더링 해주고 있었다.

react.useEffect(() => {
  // 생략

  // 이전 ns 과 현재 ns 가 다른 경우에 setter 해주고 있음
  if (ready && previousJoinedNS && previousJoinedNS !== joinedNS && isMounted.current) {
    setT(getNewT); 
  }

  // 생략
}, [i18n, joinedNS]); // ns 로 트리거

 

docs 도 깔끔하고 커스텀해서 사용하기도 좋고 왜 많이 쓰는지 다시한번 납득할수 있었다

우리 회사도 글로벌인데 영어 버전도 지원해야지!

 

언제부턴가 다국어 지원이 흔한 기능이 되었다.

전에 사이드프로젝트 할때 i18n 써서 했던거라 대수롭지 않게 생각했는데 현업해서 해보니 생각보다 협업하기 어려웠고 절차가 번거로웠다. 우리는 i18n을 json 파일로 영문 전환 버튼으로 ko.json / en.json 바꿔서 적용했는데

자동화하기 전에 절차가 어땠냐면, 

 

1. 개발하다가 ko.json 에 key value 를 추가한다

2. 검수하시는 직원께 메신저로 검수 요청을 보낸다

3. 확인 및 검수 후 답장을 보낸다

4. 검수 받은 영문을 en.json 에 key value 를 추가한다

 

위 절차가 번역물이 업데이트가 종종 일어날때마다 진행되니 번거로운건 둘째치고 업무 효율이 매우 낮아졌다.

(검수하시는 분도 본인 업무도 많은데 자꾸 요청드려서 미안했다..)

그렇게 몇번의 업데이트 후 다른거 개발하기도 바쁜데 번역에 업무시간을 자꾸 빼앗기기 싫어서 자동화하기로 했다.

 

구글 스프레드시트를 통한 다국어 번역 자동화하기

 

타 블로그에 자세히 정리도 되어있고 방법도 상세한데 나는 좀 과하게 자동화한 느낌도 나서.. 나름 간소화 버전으로 만들었는데 다른 분들 입맛에 맞을지는 모르겠다.

 

(완성본 프리 뷰)

번역물 ko.json 에 추가

 

package.json 에 npm 명령어로 js 파일 실행
구글 스프레드시트에 자동으로 업로드, 위 화면 중 검수자는 자동 번역이 별로면 검수 열에 입력하면 자동 반영

 

npm 명령어로 다운로드 받아 en.json 파일 업데이트

 

위의 방식이다. 결국 컨셉은 1. ko.json 파일을 npm 명령어로 구글 스프레드시트에 업로드 2. 검수자는 구글 스프레드시트 화면만 바라보면 되고 3. 검수 됐다는 답장오면 한번씩 npm run download en.json 을 업데이트 해주면 된다.

 

절차 단계가 줄어든건 아니지만 업데이트 된 번역물만 뽑아서 검수자에게 전달하고 검수된 내용을 json 파일로 복붙하던 DX 개선과 특히, 같은 스프레드시트 화면을 바라보는 검수자와의 커뮤니케이션 비용은 확실하게 개선되었다.  

 

아래부터는 구글 스프레드 시트와 연동하여 다국어 번역 절차를 간소화하는 방법을 서술하겠습니다.

 

1. 구글 스프레드시트 서비스 계정 만들기

구글 클라우드 대시보드 요 링크 타고 들어가서 아래 <서비스 계정> 클릭

 

2. 계정 이름 정도만 입력하고 완료 누르면 된다. 

 

 

3. 만들어진 서비스 계정 옆에 편집 버튼을 눌러 키를 추가한다 (키 유형 JSON)

※ 만들어진 json 파일은 api 사용할때 인증해야되니 저장해두자

 

4. api로 컨트롤할 스프레드시트 만들기

구글 스프레드시트 바로가기

링크 타고 빈 스프레드시트 하나 만들어준다.

아래 밑줄 친게 5번에서 사용할 process.env.SPREAD_SHEET_DOC_ID 부분이다.

공유 버튼 눌러서 만들어 두었던 사용자 계정 이메일을 등록!

참고로)

자동번역 열에 등록해 놓은 함수는 =IF(B2<>"",GOOGLETRANSLATE(B2, "ko", "en"),"")

다운로드 열에 등록해 놓은 함수는 =IF(D2<>"", D2,C2)

 

 

5. upload.js 만들기

npm i google-spreadsheet
npm i google-auth-library
npm i dotenv // (option!)
import dotenv from "dotenv";
dotenv.config();

import { GoogleSpreadsheet } from "google-spreadsheet";
import { JWT } from "google-auth-library";
import creds from "./translation/.credentials/august-sandbox-378002-e13c063fc08e.json" assert { type: "json" };

// 위에 import 는 3번에서 만든 json key 에서 불러온것임!

import fs from "fs";

const serviceAccountAuth = new JWT({
  email: creds.client_email,
  key: creds.private_key,
  scopes: [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive.file",
  ],
});

const doc = new GoogleSpreadsheet(
  process.env.SPREAD_SHEET_DOC_ID,
  
  // SPREAD_SHEET_DOC_ID 는 4번에서 밑줄친 부분!
  
  serviceAccountAuth
);

await doc.loadInfo();
console.log(doc.title);

const sheet = doc.sheetsByIndex[0]; // 난 첫번째 시트가 번역 시트여서 [0] 설정한 것

await sheet.setHeaderRow([ // 첫번째 행 header 설정 해주고 
  "key",
  "한글",
  "자동번역",
  "검수",
  "비고",
  "다운로드",
]);

const rows = await sheet.getRows();

let existedKeys = []; // 이미 시트에 등록된 key는 제외해주려고 배열에 담음
for (const row of rows) {
  existedKeys.push(row._rawData[0]);
}

const filePath = process.env.KO_JSON_PATH; // 사용하실 분들은 이 경로 설정만 하시면 됩니다
const jsonFile = fs.readFileSync(filePath);
const jsonData = JSON.parse(jsonFile);

// json key의 depth를 .으로 평탄화하는 재귀함수
function flattenObject(obj, parentKey = "", result = []) { 
  for (const [key, value] of Object.entries(obj)) {
    const newKey = parentKey ? `${parentKey}.${key}` : key;
    if (typeof value === "object" && value !== null && !Array.isArray(value)) {
      flattenObject(value, newKey, result);
    } else {
      const newObject = { key: newKey, 한글: value };
      result.push(newObject);
    }
  }
  return result;
}

const flattenedKoJson = flattenObject(jsonData);

// 이미 있는건 업데이트 해주지 않습니다. (있던 내용을 바꾸려면 그냥 시트에서 바꾸세요!)
const newFlattenedKoJson = flattenedKoJson.filter(
  (obj) => !existedKeys.includes(obj.key)
);

console.log("새롭게 업데이트 될 Array: ", newFlattenedKoJson);

if (newFlattenedKoJson.length > 0) {
  await sheet.addRows(newFlattenedKoJson);
}

console.log("Uploaded successfully");

 

 

6. download.js 만들기

import dotenv from "dotenv";
dotenv.config();

import { GoogleSpreadsheet } from "google-spreadsheet";
import { JWT } from "google-auth-library";
import creds from "./translation/.credentials/august-sandbox-378002-e13c063fc08e.json" assert { type: "json" };
import fs from "fs";
import { dirname } from "path";

const serviceAccountAuth = new JWT({
  email: creds.client_email,
  key: creds.private_key,
  scopes: [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive.file",
  ],
});

const doc = new GoogleSpreadsheet(
  process.env.SPREAD_SHEET_DOC_ID,
  serviceAccountAuth
);

await doc.loadInfo();

const sheet = doc.sheetsByIndex[0];

const rows = await sheet.getRows();
const fetchedRows = rows.map((row) => {
  return row._rawData;
});

function arrayToNestedObject(arr) {
  const result = {};

  arr.forEach(([path, , , , , value]) => {
    const keys = path.split(".");
    let current = result;

    keys.forEach((key, index) => {
      if (index === keys.length - 1) {
        current[key] = value;
      } else {
        if (!current[key]) {
          current[key] = {};
        }
        current = current[key];
      }
    });
  });

  return result;
}

const nestedObject = arrayToNestedObject(fetchedRows);

const filePath = process.env.EN_JSON_PATH; // 마찬가지로 사용하실 분들은 요 경로를 설정해주시면 됩니다!
// 중간 폴더 없으면 생성
fs.mkdirSync(dirname(filePath), { recursive: true });
// 파일 생성
fs.writeFileSync(filePath, JSON.stringify(nestedObject, null, 2));

console.log("Downloaded successfully");

 

담백하게 적어서 금방 끝나버렸는데 내용이나 컨셉 설명은 타 블로그에 정리 잘된게 많으니 아래 링크를 보시는걸 추천드립니다.

https://velog.io/@calvinsnax/getting-i18n-with-google-spreadsheet

 

구글스프레드시트를 200% 활용한 국제화(i18n) 자동화 사례

비개발자와의 협업이 유독 중요했던 국제화 프로젝트. 구글스프레드시트를 활용하여 비개발자와의 협업을 멋지게 완성해낸 국제화(i18n) 자동화 사례를 소개합니다.

velog.io

https://www.wooslog.com/blog/internalization-automation

 

구글 스프레드시트를 통한 국제화 자동화

뤼튼에서는 글로벌한 유저들에 대비하기 위해 국제화를 진행하였습니다. 국제화는 한국어로 된 문구들을 다른 나라 언어로도 제공하는 기능을 말합니다.

www.wooslog.com

 

2탄은 react 에 적용하는 부분입니다:)

여기까지만 봐도 핵심 내용은 끝이긴하지만..

 

git init 부터 하고
git remote add [원격url] 원격 저장소 추가 or git clone으로 가져옴
git add . or [파일이름] 모두(파일이름) 커밋 대상에 추가하고
git commit -m '커밋메세지' 커밋 대상에 추가된 애들을 커밋한다
git pull origin 원격 저장소의 모든 데이터를 가져와 로컬저장소와 merge한다
git push origin 원격 저장소에 데이터를 보낸다.
git-hub 사이트에서 pull request를 통해 merge한다

Git flow전략의 정의는 '브랜치의 생성, 삭제, merge 등 git의 유연한 구조를 활용해 협업을 유연하게 하는 방법론' 정도이다.

그냥 어떻게 사용하고있는지 잘 사용하는 '사례'를 참고한다~ 라고 생각하자.

 

Git-flow전략이라고 하면 nive가 고안한 Git-flow가 가장 대표적인데 그건 재쳐두고

 

GitHub-flow와 GitHub-flow를 보완한 GitLab-flow를 정리해볼까 한다. 성격이 나한테 맞기도하고

GitHub-flow는 Git-flow의 복잡성을 들어냈다. 브랜치는 master 하나만 남겨두고 나머지 브랜치는 개발자 재량에 맡겨 사용하기 가볍다.

 

GitHub-flow 정책

1. master은 언제든지 배포 가능하다.

2. 새로운 프로젝트는 master를 기반으로 별도 브랜치를 생성하여 작업을 진행한다.

3. 브랜치는 로컬에 commit 하고, 정기적으로 원격 브랜치에 push 한다.

4. 피드백이나 도움이 필요하거나, 코드 병합 준비가 됐으면 pull request한다.

5. 다른 사람이 변경된 코드를 검토한 뒤 승인하면 master에 병합한다.

6. 병합된 master는 즉시 배포 할 수 있고, 실제로 배포한다.

 

GitHub-flow는 상시 배포 모델이다. 장기간 프로젝트와 핫픽스 등 유지보수 작업이 필요하면 Git-flow가 적합하지만,

상시 배포하는 팀은 간단한 GitHub-flow가 적합하다.

 

적다보니까 하던 작업만해서 지식이 부족하구만, 생활코딩 센세 강의 듣고 마저 작성하자~

https://www.youtube.com/watch?v=EzcF6RX8RrQ 

 

 

12/16 TIL

React_query를 배웠다. 뭔가 실전프로젝트 때 날 괴롭히던 녀석이라 그런지 언젠가부터 꽃 모양만 보면 '아, 리액트쿼리' 가 머리에 떠올랐다. 이틀에 거쳐 데이터 패칭이랑 캐싱 등 가지고 놀아보니 별거 아니었고 생각보다 api를 쉽게 사용해 주는 좋은 녀석이었다. 요새 이력서 안내고 면접 공부도 조금 뜸하게 하면서 타입스크립트랑 공부만하는게 왜 그른가~ 생각해보니까 그동안의 포트폴리오가 너무 아쉽다

지금 작성하는 코드가 전에 배포에 급급해서 작성하던 코드랑은 질이 다른걸 스스로 느끼고있어서 그걸 포트폴리오로 내고 싶지 않다- 는 생각이 좀 든다.(뭐 이런 생각하는거보니 아직 돈이 안 급한가보다..)  

노마드코더 강의 이제 1주일 정도면 완강할거 같은데 끝내고 개인 프로젝트 하나 1주일동안 만드는걸 끝으로 취업 전선으로 돌아가련다. (는 페이크고 1달째 포트폴리오 더 만들면서 공부중.. 하 NEXT.js 재밌잖어.. 1달동안 실력 엄청 늘었다 너..? _1월12일의 내가 12/16일의 나에게..)

Server Side Rendering은 서버에서 모든 데이터를 작성해서 클라이언트로 전송 -> 클라이언트는 해당 데이터를 웹사이트에 표시하는 방법의 웹 통신 방법입니다. 라고 하면 좀 관념적이니까

 

=>서버에서 페이지 내용을 다 그려서 브라우저로(클라이언트) 던져준다 고 생각하자.

장점으로는 HTML에 모든 내용이 담겨있어 효율적인 SEO(검색최적화)가 가능하고.

웹 사이트에 접속하면 서버에서 내용을 다 그려서 브라우저로 주니까 속도는 빠른데 HTML 파일만 가져온 상태고 자바스크립트는 다운받는데 시간이 조금 더 걸리게된다. 그래서 화면이 보여지는 시간(TimeToView)과 동작 시간(TimeToInteract) 이 달라 사용자가 버튼 같은거 클릭하면 작동 안하는 등 안좋은 사용자 경험이 있을 수 있다.

또, 랜더링의 많은 과정이 서버 사이드에서 일어나기 때문에 사용자가 많을 수록 서버 부담이 커진다.

 

렌더링 방법의 등장 순서

HTML을 이용한 웹이 시작되고 앵커 태그 <a href ="URL"/> 를 이용한 통신은 기본적으로 아래 과정을 거치는 정적 static sites 이었다. (서버와 클라이언트가 HTML파일을 주고받아 매번 페이지 전체가 새로고침됨.)

근데 쓰다보니까 새로고침이 사용자에게 많이 거슬려서 CSR -> SSR -> SSG이 나오게 됐다.

(CSR 개발을 위한 라이브러리로는 React, 프레임워크로는 Angular, Vue.js 등이 있다.)

(SSG 대표 라이브러리는 Gatsby, NEXT.js)

프로젝트에 SSR을 도입하려면 TTV와 TTI의 시간차이를 줄이기 위해 어떻게 할지 새로고침을 조금더 매끄럽게해서 더 나은사용자 경험을 하는데에 고민하는 것이 좋을듯하다.

 

추가

 React는 CSR에 최적화 되어있지만 Gatsby 라이브러리와 함께 사용하면 정적으로 웹페이지를 미리 생성해두고 서버에 배포할 수 있다. 이 웹사이트는 정적으로 보이지만, 추가적으로 서버에서 데이터를 받거나 동적으로 처리해야 하는 로직은 자바스크립트 파일을 함께 가지고 있을 수 있어서 동적인 요소도 충분히 추가 가능하다.

NEXT.js는 강력한 SSR을 지원하는 라이브러리였는데, 요즘에는 CSR과 SSR을 섞어서 목적에 맞게 사용할 수 있다.

 

 

 

12/15 

춥다  Winter is coming.. 아침부터 눈와서 마음이 꺾일뻔했다 스터디 카페에 도착하니 젖은 신발이 바닥을 검은 진흙으로 물들였다. 뭐 그것도 잠시고 커피마시면서 책좀 읽으니까 금방 평온해졌다. 1984 보고있는데 재미가 쏠쏠하다. '공산주의 멸망편'으로 과장스러운 장면도 많지만 과거 공산주의나 지금의 중국 북한과 크게 다르지 않은 생각도 들어 지금 커피마시면서 독서 할 수 있는 사실이 감사하게 느껴졌다.

연말이라 송년회 약속이 이것저것 잡혀있다. 간만에 얼굴 보느라 좋기는 한데 한편으론 좀 부담스럽다. 빨리 취업해야지

 

Q. RESTful 하다는 것은 어떤 건가요?

> REST를 잘 지킨 시스템입니다. RESTful의 목적은 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것 입니다.

 

 REST에 대해서 말씀드리면, 웹에서 데이터를 전송하고 처리하는 방법을 정의한 인터페이스입니다. 모든 데이터의 구조와 처리방식은 URI을 통해 정의되기 때문에 직관적으로 이해하기가 좋습니다.

 

REST의 아키텍쳐는 고유한 ID로 명시한 자원과 HTTP Method인 행위 Client요청에(request) 대한 CRUD 응답(response)인 표현으로 구성되어있습니다. 

 

특징은 서버 클라이언트 구조 / 무상태성 / 캐시 처리 가능/ 계층화 / 인터페이스 일관성이 있습니다.

순서대로 간략히 말씀드리면,

 1) 서버와 클라이언트의 역할을 확실히 구분시켜주어 서버 클라이언트 간 개발해야 할 내용이 명확해지고 서로 의존성이 줄어들게 됩니다. (서버는 api 제공, 클라이언트는 인증이나 컨텍스트(세션, 로그인 정보)등을 직접 관리)

 2) REST는 무상태성입니다. 작업을 위한 상태정보(세션정보나 쿠키 정보등)를 따로 저장하고 관리하지 않기 때문에 API 서버는 들어오는 요청(respones)만을 단순히 처리하면 됩니다. 그래서 서비스의 자유도가 높아지고 구현이 단순해집니다.

 3) 캐싱기능은 HTTP 웹 표준을 그대로 사용하기 때문에, HTTP 프로토콜 캐싱 기능인 Last-Modified 태그나 E- Tag를 이용하면 캐싱 구현이 가능합니다.

 4) 계층화는 REST는 다중 계층으로 구성될 수 있습니다. 보안, 로드 밸런싱, 암호화 계층을 추가해 구조상의 유연성을 만들 수 있고 PROXY, 게이트웨이 같은 네트워크 기반의 중간매체를 사용할 수 있습니다.

 5) 마지막 특징 인터페이스 일관성은 HTTP 표준만 따르면 안드로이드,IOS 플랫폼 등등 특정 언어나 기술에 상관없이 모든 플랫폼에서 사용할 수 있습니다. 

 

단순히 하나의 브라우저만 지원하던 이전과 달리, 최근 서버 프로그램은 여러 웹 브라우저와 아이폰, 안드로이드 등 다양한 애플리케이션과 통신할 수 있어야합니다. 이런 상황에 범용적이고 쉽게 사용 가능한 REST API 방식이 생겨났고 지금까지 해당 방식이 많이 쓰이고 있습니다.

 

그럼에도 몇몇 단점이 있어 이를 보완하기 위해 GraphQL이 등장했고 프로젝트 성격에 따라 어느 방식을 사용하는 것이 좋은지 고려하는것이 좋다고 생각합니다. 

 

 

 


12/9 TIL

진~짜 간만에 TIL! 요새 너무 바빴당 아침부터 밤까지 공부하고 취준하고 운동하고 책읽고 열심히 사는중.

git에 잔디 쌓이는 재미가 쏠쏠하당 자바스크립트로 게임 만드느라 3일정도 홀라당 코딩하고 간만에 책읽는 재미 붙어서 11월에만 5권은 읽은거 같다. 뭐 요번주부터는 진짜 취업해야지.. 생각들어서 이력서 적당히 내면서 개인프로젝트 하고 있는데 노마드코더강의 들은거 진짜 잘한거 같당 그동안 머리박으면서 코딩했던거 개념 차근차근 알려주고 꿀팁 알려줘서 아 이렇게 하면 컴팩트하게 코드 작성 하겠구나~ 싶어서 지금 단계에 맞는 강의를 찾은듯.

 

뭐 공부하고 운동하고 재미없게 살아서 스페셜한 일은 없었는데, 우리 월드컵16강 진출한거 띠용해서 16강전 친구네 놀러가서 같이 본거 정도? 아니 포르투갈 이기고 우르과이가 가나 2골차이로 이겨서 16강 진출한거 진짜 에바아님? 드라마도 스토리 이정도면 짜친다는 소리 들을거 같은데, 증말 중요한건 꺾이지 않는 마음이었나보다. (유투브에 ㅈ만쳐도 '중요한건 꺾이지 않는 마음'이 뜬다 올해 슬로건..)

물론 새벽4시에 일어나서 본 브라질 전은 4대1로 떡실신 당하긴 했지만 거기까지 간 스토리만으로도 충분히 감동적이고 대단했고 인터뷰에서 오히려 져서 죄송하다고 사과하는 선수들을 보며 전혀 그렇게 생각 안한다고 말해주고 싶었당

그리고 후반전은 너무너무 잘했음 국대들 짱짱맨..bb

 

 

Q. Async/Await와 Promise의 차이

⇒비동기 처리를 다룰 수 있는 방법이다. (+ Callback 도 있음)

 

차이점

1) 에러 핸들링

Promise를 활용할 시에는 .catch() 문을 통해 에러 핸들링이 가능하지만,

async/await 는 에러 핸들링 할 수 있는 기능이 없어 try-catch()문을 활용해야 한다.

 

2) 코드 가독성

Promse의 꼬리를 무는 .then() 지옥의 가능성

async/await는 비동기 코드가 동기 코드인 것처럼 흐름을 이해하기 쉽다. 코드가 길어질 수록 promise보다 가독성이 좋다.

 

 

정의

Promise

⇒자바스크립트 비동기 처리를 할수 있게 해주는 객체

대기/ 이행/ 실패 3가지 상태가 있다.

비동기 처리가 완료 되지 않았다면 대기(Pending), 완료 되었다면(Fulfilled), 실패하거나 오류가 발생하였다면 Rejected 상태를 갖는다.

 

async / awiat

⇒ 최근 문법으로 callback 이나 Promise의 콜백 지옥 단점을 보완한다.

await는 async함수 안에서만 동작한다.

 


Q. useEffect와 useLayoutEffect 차이에 대해 설명해주세요

 

차이점

1) 컴포넌트 랜더링

useEffect의 경우 컴포넌트 랜더링 -> 화면이 먼저 업데이트 된 후 useEffect를 실행하고

useLayoutEffect는 컴포넌트 랜더링 -> useLayoutEffect 실행 후 화면이 업데이트 됩니다.

 

2) 동기 / 비동기

useEffect는 비동기적으로 실행하고

useLayoutEffect는 동기적으로 실행합니다. => 로직이 복잡할 경우 시간이 오래 걸림.

 

useLayoutEffect 사용 이유

=> useEffect는 DOM이 화면에 그려진 이후에 useEffect 함수를 호출합니다. 화면이 복잡해지면 느려지는 것을 체감할 수 있을정도로 렌더링 시간이 증가하게 됩니다. 그러면 state 초기값을 보게 되버립니다.그래서 useLayoutEffect를 써서 화면에 DOM이 그려지기 전에 호출해서 초기값을 보는 문제 등을 방지 합니다. 


Q. Closure 란?

 

클로저는 자바스크립트 고유 개념이 아니라, 여러 함수형 프로그래밍 언어에서 공통적으로 발견되는 특성이다.

따라서 각기 다른 방식으로 설명하니 정의를 암기하기보다 사례를 이해하고 나만의 정의를 내리는것이 좋다.

⇒ 일단 사전적 정의는 외부함수에 접근할 수 있는 내부 함수 or 이러한 개념.

아래 코드와 같이 함수가 함수를 감쌀때의 외부함수와 내부함수인데 이때 fn3() 엔 l3의 변수 밖에 없지만 콘솔엔 l0, l1, l2, l3가 잘 찍힌다.

-> fn3()실행시에 로컬 스코프엔 변수는 l3 밖에 없지만 closure라는 개념으로 외부함수에서 선언한 변수를 땡겨왔다.

function fn1(){
    function fn2(){
        function fn3() {
            let l3 = 'l3';
            console.log(l0, l1, l2, l3) // l0, l1, l2, l3   <--breakpoint
        }
        let l2 = 'l2';
        console.log(l0, l1, l2)
        fn3();
    }
    let l1 = 'l1';
    console.log(l0, l1);
    fn2();
}
fn1();

디버거로 console.log(l0, l1, l2, l3)에 브레이크포인트를 걸었을 때 스코프는 다음과 같다.

closure라는 개념으로 l2와 l1의 변수를 땡겨오는 모습.

(자바스크립트 변수 가져오는거(객체 prototype과 동일) local 없으면-> script 없으면 -> global인데 Closure있으면 local 다음에 끼어든다)

 

 

 

다우기술 이력서 내느라 일기를 안썼넹 이것저것 하느라 면접준비 자꾸 뒷전이라 큰일.. 뭐 강의도 듣고있고 1일1커밋 중인데 잔디 채워지는게 생각보다 쏠쏠하다 자바스크립트랑 리액트 이론 공부할 때마다 느끼는데 머리박으면서 코드부터 짜서 이론 진짜 부족한거같당ㅋㅋㅋ그래도 재밌게 시작했으니 후회는 없어..☆

요새 책 1시간씩 보고있는데 너무 재밌당 내가 '싫고 좋고 이상하고' 읽고 나서 세상 이슈에 대해 더 알고 싶어졌고 '다정한 것이 살아남는다' 보고 인류학 관련된 책을 더 찾아보고 싶었고 지금은 '정의란 무엇인가' 보면서 독서 뽕맛 제대로 느끼고있다. 마이클 샌델 천재아님? 철학적인 주제들을 이렇게 재밌게 풀어내는거 할아버지 폼 미쳤음 진짜 꼭보세요 추천..b 

(잠깐 찾아보니까 진짜 천재 맞네요 27세 최연소 하버드 교수되셨네ㅎㅎ)

 

며칠 좀 오락가락 하는중이다 나같은 고민 하는 사람 많을거 같은데, 지금 이 시점에 강의를 듣는게 맞는지 블로그 쓰는게 맞는지 책을 읽고있는게 맞는지 궂이 지금 이시점에 해야하는건지 뭐 그런 쓸데없는 생각? 극 S라 별 고민 안하고 한가지 씩 하는 성격인데 뭐 이력서 슬슬 공장처럼 던져야되는데 좀 더 준비하고 있으니까 잡 생각이 끼어드는거 같다. 뭐 다우기술 자소서 작성하면서 좀 리뉴얼했으니까 월요일부턴 던져보자...ㅎ 

요새 댕기는 스터디카페 이벤트하길래 겸사겸사 글쓴당

 

처음에 어떻게 오게됐드라?

항해99할때 팀원이 종종 스터디카페 간다길래 난 그당시엔 너~~~~무 바빠서 집에서도 할거 개많은데 어떻게 카페랑 왔다갔다 하시징 했는데 항해 끝나니까 귀신같이 의욕 떨어져서 여기저기 카페 기웃거리다가 여기까지 오게된거같다.

뭐 여기가 종착역이었음. 

군자 열공다방

 

진짜진짜진짜 스터디하기 딱 좋은 환경 만들어 놓으셔서 '군자' 근처에 사시는 분 들은 꼭 이용해보세요 최고..b

 

우선 기본 독서실 장소와 노트북 장소가 나뉘어져서 나같은 노트북러들은 글로 가면되고 백색소음 같은 걸 알잘딱 볼륨으로 깔아두셔서 너무 사부작 거리는 소리는 잘 묻혀서 신경안쓰임  

나뉘어져있는 독서실(좌) / 노트북(우) 장소

커피머신 있어서 (심지어 원두 괜찮은거 씀..) 카페인 걱정 1도 없고 4종류나 되는 음료수랑 여러 티백들(히비스커스, 보이차 등등), 소소한 간식까지 있어서 간간히 분위기 전환하기도 쏠쏠하다.

커피머신과 각종 티백 음료수들..b

난 카페에서 2시간 정도 작업할 때도 의자랑 책상 정말정말정말 중요하게 생각해서 괜찮은 카페 찾으면 거기 밖에 안갈 정돈데 여긴 의자도 편하고 아래에 드르륵 소리 안나라고 헝겊 같은걸로 덧씌워두셔서 정말 사장님 디테일 하나하나 확인하고 놀랐다. 뜬금없지만 분명 공부 엄청 잘하셨을듯 .

 

그리고 또 중요한게 고등학생 이상부터 이용가능해서 잼민이들은 컷이고(개좋음) 요렇게나 잘 해놓으니까 오는 사람들도 다들 간절하신 분들이라 집중력이 자동으로 업 된당.

디테일한 부분 말하면 끝이없는데 직접 안보면 장점이 다 안담기니께 오셔서 확인해보면 좋을거같당 :)

 

마지막으로 여기 오자마자 보고 기분이가 좋아졌던 열공러들의 포스팃..☆ (개 귀여움) 

열심히 하는거 이쁘다, 다들 화이팅하자구~

'잡담' 카테고리의 다른 글

React 시작시 기본적으로 설치해야되는거!  (0) 2022.08.13
4. 나만의 프로젝트  (0) 2022.07.28
3. 나만의 프로젝트  (0) 2022.07.28
2. 나만의 프로젝트  (0) 2022.07.28
1. 나만의 프로젝트  (0) 2022.07.28

+ Recent posts