기간 : 2021.09.06~2021.10.06
기존 모인 서비스는 react 초창기 SSR 방식과 CSR 방식을 조합하여 만든 웹페이지 였음
해외송금 서비스 이기 때문에 핵심기능이 송금기능이었는데
사용자가 송금보낼 국가, 송금방법, 송금받는사람에 따라서 정보를 입력해서 제출하면 끝이었음
사용자는 송금을 보내고 나서 사이트에 머물지 않고 바로 이탈하곤 했음
어쩌다 사이트에 방문하는 것이 아닌 해외송금을 보내려는 확실한 목적을 가지고 사이트에 방문하기 때문에 SEO 가 크게 필요하지 않았음
그럼에도 불구하고 SSR 방식으로 하다보니 아래와 같은 단점들이 생겼음
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}
/>
.....
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 }}
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