[dev-log] 송금과정 개편 ( CSR 전환 )

김_리트리버·2022년 5월 12일
0

devlog

목록 보기
1/2

서버사이드 렌더링( SSR ) 에서 클라이언트 사이드 (CSR) 렌더링으로 전환

기간 : 2021.09.06~2021.10.06

Situation

기존 모인 서비스는 react 초창기 SSR 방식과 CSR 방식을 조합하여 만든 웹페이지 였음

해외송금 서비스 이기 때문에 핵심기능이 송금기능이었는데

사용자가 송금보낼 국가, 송금방법, 송금받는사람에 따라서 정보를 입력해서 제출하면 끝이었음

사용자는 송금을 보내고 나서 사이트에 머물지 않고 바로 이탈하곤 했음

어쩌다 사이트에 방문하는 것이 아닌 해외송금을 보내려는 확실한 목적을 가지고 사이트에 방문하기 때문에 SEO 가 크게 필요하지 않았음

그럼에도 불구하고 SSR 방식으로 하다보니 아래와 같은 단점들이 생겼음

  • 배포 속도가 느림
    jenkins 통해 배포를 완료하는데 약 15~20 분 소모되었음
  • 앞으로가기, 뒤로가기시 데이터 초기화
    해외송금시 입력해야 하는 정보는 국내 송금보다 많고 다양한데
    사용자 입장에서는 입력을 다해놓고 잠깐 수정하려고 뒤로가기를 클릭했는데
    메인 페이지로 이동해버리는 불편함이 있었음
  • SEO 가 크게 필요하지 않는데 EC2 instance 를 개발, 운영 각각 사용하여 AWS 비용이 낭비되고 있었음
    s3, cloudfront 를 사용하는데 비해 굳이 필요없는 비용이 낭비된다고 생각했음
  • 사용자의 권한 및 정보에 따라 특정 url 로 redirect 시키는 경우 server 와 client 각각 code 가 있어 bug 를 유발할 가능성이 있음
  • client 에서 history API를 사용하는 부분이 react-router 를 사용하지 않고 redux 와 연관하여 사용하고 있어 가독성 떨어지고 관리하기 힘듦
  • express 를 사용한 서버였는데 url 의 params 정보를 가져오는 등 단순한 기능만을 하고있었음 이는 react-router 등을 사용해서 client 에서도 가능한 부분이었음

Task

  • node server 의 기능을 client 로 이전
    • cookie 를 사용한 데이터 초기화
    • redux store 를 서버에서 생성하여 html DOM 에 포함하는 부분 제거
  • s3-cloudfront 사용한 CI-CD
  • react-router 로 routing 관리 일원화
  • redux 에서 routing 관리하는 부분 제거

Action & Result

  • window history api, reducer, server 에서 url 관리하는 부분을 삭제하고 react-router 로 변경함

before

// server.js 
...
router.get('/', (req, res) => send('landing', req, res))
router.get('/ui-test', (req, res) => send('ui-test', req, res))
router.get('/login', (req, res) => send('login', req, res))
router.get('/signup', (req, res) => send('signup', req, res))
router.get('/business', (req, res) => send('business', req, res))
router.get('/business/signup', (req, res) => send('business/signup', req, res))
router.get('/info', (req, res) => send('info', req, res))
router.get('/company', (req, res) => send('company', req, res))
router.get('/faq', (req, res) => send('faq', req, res))
router.get('/notice', (req, res) => send('notice', req, res))
router.get('/privacy', (req, res) => send('privacy', req, res))
router.get('/terms', (req, res) => send('terms', req, res))
router.get('/main', (req, res) => send('main', req, res))
router.get('/mypage/verification', (req, res) => send('mypage/verification', req, res))
router.get('/mypage/discount', (req, res) => send('mypage/discount', req, res))
router.get('/mypage/coupon', (req, res) => send('mypage/coupon', req, res))
router.get('/mypage', (req, res) => send('mypage', req, res))
router.get('/setting', (req, res) => send('setting', req, res))
router.get('/reversed-remit', (req, res) => send('reversed-remit', req, res))
router.get('/referral', (req, res) => send('referral', req, res))
router.get('/invite/:code', (req, res) => send({ view: 'invite', referCode: req.params.code }, req, res))
router.get('/share/:uuid', (req, res) => send({ view: 'share', remitUuid: req.params.uuid }, req, res))
router.get('/remit/history', (req, res) => send({ view: 'remit/history' }, req, res))
router.get('/remit/update/:id', (req, res) => {
  const state = {
    view: 'remit/update',
    remitId: Number(req.params.id),
  }
// History.js 
static pushState({ action, state, prevView }) {
    if (!isMounted) return;

    let url;
    switch (state.view) {
      case 'landing':
        url = '';
        break;
      case 'recipients/update':
        url = `${state.view}/${state.recipientId}`;
        break;
      case 'remit/update':
        url = `${state.view}/${state.remitId}`;
        break;
      case 'shared':
        url = `shared`;
        break;
      case 'reversed-remit':
        url = `reversed-remit`;
      case 'invite':
        url = `${state.view}/${state.referCode ? state.referCode : ''}`;
        break;
      case 'ui-test':
        if (process.env.NODE_ENV === 'production') {
          url = '';
          break;
        }
        url = `ui-test`;
      default:
        url = `${state.view}`;
        break;
    }

    if (action.type === PUSH_VIEW || (action.type !== POP_VIEW && state.view !== prevView)) {
      _History.toggleChatRemit(state.view);

      const _pushState =
        prevView === 'pending' ? (...args) => window.history.replaceState(...args) : (...args) => window.history.pushState(...args);

      _pushState({ view: state.view, param: action.param }, state.view, `${state.urlPrefix}/${url}`);

      if (window.ChannelIO) {
        window.ChannelIO('track', `[PUSH] ${url}`);
      }
    } else if (action.type === POP_VIEW && state.view !== prevView) {
      window.history.replaceState({ view: state.view, param: action.param }, state.view, `${state.urlPrefix}/${url}`);

      // RemitStep에서는 뒤로가기시에 앞으로 가기 버튼을 disabled하기 위해 pushState를 한다.
      if (state.view.includes('remit') && action.param && action.param.from === 'back') {
        window.history.pushState({ view: state.view, param: action.param }, state.view, `${state.urlPrefix}/${url}`);
      }
    }
  }
// reducer.js 
export const reduce = (...params) => {
  const prevView = params[0].view;
  const state = _reduce(...params);
  const action = params[1];

  if (
    [LOGIN, UPDATE_LOGIN_STATUS].includes(action.type) &&
    state.loggedIn &&
    (!state.profile.firstname.length || !state.profile.phone2.length || !state.profile.agreed)
  ) {
    // user info scarce && social login
    state.view = 'profile/update';
    state.nextView = null;
  } else if (state.univAuthCode && state.loggedIn) {
    state.view = 'event/univ/country/verify';
    state.nextView = null;
  } else if (state.loggedIn && [LOGIN, UPDATE_LOGIN_STATUS].includes(action.type) && state.nextView) {
    // Redirect after login!
    state.view = state.nextView ? state.nextView : 'main';
    state.nextView = null;
  } else if (state.loggedIn && SHOULD_NOT_LOGIN_URLS.includes(state.view)) {
    state.view = 'main';
    state.nextView = null;
    window.history.replaceState({ view: state.view, param: action.param }, state.view, `${state.urlPrefix}/${state.view}`);
  } else if (!state.loggedIn && LOGIN_REQUIRED_URLS.includes(state.view)) {
    if (state.view !== 'pending') {
      state.nextView = state.view;
    }

    state.view = state.loggedIn === null ? 'pending' : 'login';
    if (state.view === 'login') {
      window.history.replaceState({ view: state.view, param: action.param }, state.view, `${state.urlPrefix}/${state.view}`);
    }
  }

  pushState({ action, state, prevView });

  return state;
};

after

// MainView.tsx
...
    <Switch>
      <NotLoginRequiredRoute
        exact
        path={SHOULD_NOT_LOGIN_URLS}
      >
        <NotLoginRequiredView />
      </NotLoginRequiredRoute>
      <LoginRequiredRoute
        exact
        path={LOGIN_REQUIRED_URLS}
      >
        <LoginRequiredView />
      </LoginRequiredRoute>
      <Route
        exact
        path='/info'
        component={Info}
      />
      <Route
        exact
        path='/company'
        component={Company}
      />
      <Route
        exact
        path='/faq'
        component={FAQ}
      />
      <Route
        exact
        path='/notice'
        component={Notice}
      />
      <Route
        exact
        path='/privacy'
        component={Privacy}
      />
      <Route
        exact
        path='/terms'
        component={Terms}
      />
      <Route
        exact
        path='/share/:uuid'
        component={RemitShare}
      />
      <Route
        exact
        path='/promotion'
        component={Promotion}
      />
      <Route
        exact
        path='/promotion/detail/:id'
        component={PromotionDetailPage}
      />
      <Route
        exact
        path='/currency/info'
        component={CurrencyInfo}
      />
      <Route
        exact
        path='/currency/info/:unit'
        component={CurrencyInfo}
      />
.....
  • 기존 EC2 instance 를 대체할 S3, Cloudfront 조합으로 CI-CD 구성

s3 만을 사용할 경우 SPA 를 제공하기 위해 public 으로 설정해야 한다는 단점이 있어 cloudfront 의 OAI 를 사용해서 cloudfront 만 s3 에 접근할 수 있도록 하였다.

CI 때는 unit test, webpack build 까지만 수행하고 CD 때 S3 sync 로 최신 build 파일을 반영하고 cloudfront cache invalidate 통해 전세계 edge 의 cache 를 무효화 시켜 최신파일을 받아볼 수 있게 하였다.

새로운 라이브러리를 설치할 일이 적어 node_modules 를 cache 해놓아

github action 시간을 단축 시켰다.

name: moin web front development CI-CD

on:
  push:
    branches:
      - new-develop

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - name: Checkout source code.
        uses: actions/checkout@v2

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
          always-auth: true
          registry-url: https://npm.pkg.github.com/

      - name: Cache node modules
        id: npm-cache
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ runner.OS }}-moin-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-moin-

      - name: Install dependecies
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm install
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}

      - name: Run webpack build
        run: npm run build:development

      - name: Run test
        run: npm run test

      - name: Deploy to S3
        uses: jakejarvis/s3-sync-action@master
        with:
          args: --follow-symlinks --delete
        env:
          AWS_S3_BUCKET: ${{ secrets.AWS_DEV_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ap-northeast-2
          SOURCE_DIR: 'public'

      - name: Invalidate the file from edge caches
        uses: chetan/invalidate-cloudfront-action@master
        env:
          DISTRIBUTION: ${{ secrets.AWS_DEV_DISTRIBUTION }}
          PATHS: '/build/*'
          AWS_REGION: 'ap-northeast-2'
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  • 기타 server 에서 했던 사용자 데이터 초기화하는 로직을 client 로 이전시켰다.

before

// server.js 
... 
const getTargetRemit = cookie => {
  try {
    const { countryCode, unit } = JSON.parse(cookie)

    const targetExist = supportTargets.find(target => target[0] === countryCode && target[1] === unit)

    if (!targetExist) {
      return DEFAULT_REMIT
    }

    const remit = {
      component: {
        ...DEFAULT_REMIT.component,
        unit,
        countryCode,
      },
      remitId: null,
    }

    return remit
  } catch (error) {
    return DEFAULT_REMIT
  }
}
... 

after

profile
web-developer

0개의 댓글