본문 바로가기
  • 하고 싶은 일을 하자
dev

스타트업 개발팀의 React Next.js 마이그레이션 도전기

by 박빵떡 2022. 12. 28.
반응형

나는 술담화라는 스타트업 회사에 다니고 있다. 2021년 초에는 php 기반의 웹 페이지에서 React를 도입했고, 이번 2022년 5월부터 NextJs를 도입했다. React에 NextJs를 Migration 하는 과정은 쉽지 않았다. 기존에 진행하던 회사 매출에 중요한 프로젝트도 동시에 진행해야 했고, 폴더 구조와 라우터가 완전히 바뀌기 때문에 버그가 없도록 유의해야 했다. 소규모 개발 인원으로 어떻게 성공적으로 Migration 했는지 이야기해 보겠다.

 

일단 먼저 시작하기에 앞서 NextJs가 무엇인지 간략하게 설명하겠다.

(이 글은 술담화 프론트엔드팀 개발자 이병수, 정윤기 님과 함께 작성하였습니다.)

NextJs란 무엇인가?

전통적인 웹 사이트는 글 위주의 페이지였다. 사진이나 영상은 거의 없었다. 이때의 웹페이지는 주로 HTML로만 이루어졌다. 그래서 페이지 이동을 하면 서버에서 전체 페이지를 전송해 줬다. 그러나 점차 웹 사이트가 고도화되면서 한 페이지가 담는 용량이 커졌고, 매번 서버가 새로운 페이지를 전송하기가 버거워졌다.

 

이로 인해 SPA(Single Page Application)이라는 기술이 만들어졌다. 이름에서 알 수 있듯 하나의 페이지에서 동적으로 화면을 바꾸며 표현하는 기술이다. Javascript로 유저의 동작에 반응하거나(event), 웹 페이지의 형태를 바꾸거나(DOM 조작), 페이지를 이동하고(routing: history api), 서버와 통신하여 원하는 정보를 가져오기도(AJAX) 했다. 즉, 렌더링이 브라우저 단에서 이뤄지기 때문에 서버의 부하를 줄일 수 있었다.

 

SPA라고 완벽하진 않다. SPA에는 다음과 같은 단점이 있다.

  1. 검색엔진이 웹 페이지의 정보를 가져가는 크롤링 봇은 Javascript 를 실행하기 이전의 페이지를 가져간다. 따라서 홈페이지의 콘텐츠가 검색 결과에 노출되기가 어렵다. → SEO 최적화가 이루어지지 않는다.
  2. meta 태그도 1과 같은 이유로 크롤링 봇이 가져가지 못한다. → meta 태그를 이용하는 마케팅 툴을 사용할 수 없다.
  3. Javascript가 실행 완료되기 이전에는 유저가 미완성된 페이지를 보기 때문에 UX 적으로 느리다는 느낌을 받을 수 있다.

기존 술담화 홈페이지는 React 프레임워크를 사용하고 있었는데 이는 SPA 기술이다. 위에서 나열한 단점들이 존재했다. 브라우저에서 Disable Javascript를 설정하면 하얀색 빈페이지가 나오는 것을 확인할 수 있었다.

 

SPA를 해결할 수 있는 기술이 SSR(Server Side Rendering)이다. 위에서 적었던 전통적인 웹 사이트의 기술을 활용한 방법이다. 브라우저에서 렌더링 하던 것을 서버 쪽으로 위임하여 렌더링 된 페이지를 시작부터 받는 것이다. 이를 통해 위의 단점들을 모두 해결할 수 있다. 전통적인 방식과 SPA의 방식의 장점만을 뽑아서 사용하는 것이라고 볼 수 있다.

 

React 프레임워크에서는 SSR을 사용하는 가장 대중적인 프레임워크는 NextJs이다. NextJs로 바꾸기만 하면 SPA의 단점을 모두 없앨 수 있다. 하지만 그 작업은 생각보다 만만치 않았다. 3월부터 시작하여 8월에 마무리된 NextJs 도전기 이야기를 계속 이어 보겠다.

 

병수님의 NextJs 첫 도전기

존잘 개발자 병수님

2022년 3월, 처음 병수님이 NextJs를 시도할 때는 다른 팀에서 NextJs에 대한 요구 사항이 있진 않았다. 개발자로서 새로운 기술에 대한 갈망과 더 나은 웹 서비스 제공이 가능할지 알아보기 위해 시도하셨다.

 

NextJs를 시도해보시던 중 SEO, meta 태그의 문제를 해결하고 성능 향상을 할 수 있다는 것을 알게 되셨다.

개발하시는 중에 타 팀에서의 요구 사항도 들어왔다.

  1. 마케팅 팀에서 마케팅 툴 사용을 위해 meta 태그에 대한 요구 사항이 들어왔다.
  2. 하이브리드 앱을 대비해 퍼포먼스 향상에 대한 요구 사항이 있었다.

이를 통해 NextJs 개발의 필요성에 대한 공감대가 개발팀 내에서 형성되었고, 바쁜 일들이 끝난 5월에 NextJs 프로젝트가 시작되었다.

 

폴더 구조

폴더구조는 Next Right Now 에서 추천하는 방식을 채택했다. Next Right Now에서는 추천하는 방식이 2가지가 있었다.

 

1. 일반적인 방법 파일의 종류에 따라 구분하는 방법인데 작은 프로젝트에 적합하다.

출처 : https://dev.to/vadorequest/a-2021-guide-about-structuring-your-next-js-project-in-a-flexible-and-efficient-way-472

2. Modules를 쓰는 방법 큰 프로젝트에 적합하다. 같은 feature 코드들끼리 group을 만든다. 공용 컴포넌트는 common 아래에 위치한다. 기능들을 modules로 분류해 각 기능 안에 api, components 배치한다.

출처 : https://dev.to/vadorequest/a-2021-guide-about-structuring-your-next-js-project-in-a-flexible-and-efficient-way-472

최종적으로 우리는 2번째 방식을 선택하였다.

 

폴더 구조는 답이 정해져 있지 않다. 자신이 만드는 프로젝트의 규모나 개발 인원 등의 변수를 고려해서 알맞은 방식을 결정해서 사용하면 된다. 폴더 구조를 정하는데 너무 큰 힘을 들일 필요는 없지만, 한 번 정하면 앞으로 쭉 가기 때문에 처음부터 확실히 정하는 게 좋다.

근데 Next 새 버전 나와서 나중에 또 다 바꿔야 한다

 

기술적인 이야기

1. Static Generation / Server Side Rendering / Client Side data fetching

fetch가 없는 페이지는 Static Generation으로 생성된다.

fetch가 있는 경우,

  • SEO가 필요한 페이지는 SSR로 생성한다. (상품 상세페이지)
  • SEO가 필요 없는 페이지는 Client Side data fetching으로 생성한다. ex) 나의 담화 와 같은 private page

2. redux persist

1) Disable Javascript 시 빈 화면 나오는 문제

새로고침 하면 redux 상태가 초기화되어 버그가 발생하는 경우가 있었다. 이를 해결하기 위해 redux persist 를 사용했다. 작동 방식은 redux 상태를 session storage에 저장하고 새로고침 시 session storage에서 상태를 state로 가져오는 방식이다.

 

그런데 redux persist를 사용하고 개발자 도구에서 disable javascript 를 했더니 빈 화면이 뜨는 버그를 우연히 발견했다. Static Generation으로 페이지가 생성되어 disable javascript를 해도 html 이 떠야 한다. (그게 NextJs를 쓰는 이유)

 

redux persist를 이미 많은 곳에서 사용하고 있었고 NextJs Migration도 많이 진행되었는데 갑자기 이 문제가 발견되어 멘붕이었다. 해결이 곧바로 되지도 않았고. 다행히 추후 해결 방법을 찾아서 잘 해결했다.

 

PersistGate 컴포넌트 자체도 JS 기반으로 돌아가기 때문에 window가 존재하는 경우에만 PersistGate로 감싸고 존재하지 않는 경우에는 감싸지 않도록 하여 문제를 해결했다.

typeof window === 'undefined' ? PersistGate not included : with PersistGate

 

2) redux persist 암호화 문제

redux persist 좋네 이거 만든 사람 머리 좋다~ 하면서 session storage를 구경했는데 모든 redux state들을 client가 볼 수 있었다. 특히 auth와 관련된 보여서는 안 되는 정보들도 보이고 있었다. 우연히 발견하게 되었는데 발견 못했으면 큰일 날 뻔… persist에서 암호화 기능을 제공해서 session storage에 암호화하여 저장하였다.

 

3) ISR (Incremental Static Regeneration)

로드시간을 단축시키기 위해 next 마이그레이션을 했는데 배포 후 상세 페이지 진입 시 오히려 더 느려진 이상한 일이 벌어졌다.

 

해당 페이지에 SSR(Server-Side-Rending) getServerSideProp 함수를 사용하여 pre-rendering이 이루어지는데 이는 체감상 렌더링 후 데이터를 받아오는 CSR보다 체감상 더 느린 결과를 초래했다. (SSR의 경우 데이터 페칭 후 완성된 페이지를 브라우저에 넘겨주는 반면 CSR의 경우 우선 페이지를 그려주기 때문이다) 이 문제를 우선순위로 검토했고 천만다행으로 next에서는 ISR이라는 함수를 제공하고 있었다.

 

getStaticPaths 함수로 빌드 시 활성화된 상품의 페이지들을 모두 정적으로 미리 생성하여 SSG의 속도 이점을 살렸고 적용 후 획기적인 로드 속도를 줄일 수 있었다.

 

4) rehydrate 시 스토어 상태 초기화 이슈

마이그레이션 진행 중에 로그인 세션이 풀려버리는 아주 치명적인 문제가 생겼다. 해당 문제는 SSR 후 rehydrate 시 기존 store를 reset 시켜 발생하였다.

 

조금 더 설명하자면, 서버의 스토어와 클라이언트의 스토어가 머지(hydrate)되면서 기존의 클라이언트 스토어를 없애버리고 서버에서 생성한 스토어가 덮어써져 발생한 문제이다.

다행히 공식문서와 위 문제를 경험한 분들이 제시해준 여러 가지 해결법이 있었는데,

  1. 서버와 클라이언트의 state 분리
  2. jsondiiffpatch를 활용해 diff를 캐치하고 적용
import {HYDRATE} from 'next-redux-wrapper';

// create your reducer
const reducer = (state = {tick: 'init'}, action) => {
    switch (action.type) {
        case HYDRATE:
            const stateDiff = diff(state, action.payload) as any;
            const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
            return {
                ...state,
                ...action.payload,
                page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
            };
        case 'TICK':
            return {...state, tick: action.payload};
        default:
            return state;
    }
};

  3. SSR시에 클라이언트에서 필요한 정보를 별도로 다시 정의해 주기

const reducer = (state, action) => {
  if (action.type === HYDRATE) {
    const nextState = {
      ...state, // use previous state
      ...action.payload, // apply delta from hydration
    };
    if (state.count) nextState.count = state.count; // preserve count value on client side navigation
    return nextState;
  } else {
    return combinedReducer(state, action);
  }
};

3번의 경우와 같이 필요한 정보를 별도로 다시 정의해주는 방법을 사용해서 hydrate시에도 필요한 정보를 유지할 수 있었다.

 

5) 제대로 동작하지 않던 eslint 문법을 수정하고 작업량이 방대한 airbnb-style-guide는 삭제하였다.

 

6) path aliases를 사용해 import를 모두 절대 경로로 바꾸었다.

path aliases 사용 시 파일이나 폴더 위치 변경 시 import 문을 바꾸지 않아도 되는 장점이 있었는데 기존 상대 경로를 모두 절대 경로를 바꾸는데 많은 시간이 소요되었다. 다시 생각해보면 가장 후순위로 두고 작업하는 게 나았을 듯하다.

 

협업 방식

  • 처음에는 워낙 많은 개발건이 있기 때문에 PR 하지 않고 브랜치에 바로 push 했다. 개발 속도는 빨라졌으나 코드 리뷰를 하지 못한 단점이 있었다.
  • 한 명이 npm run lint를 실행하면서 lint 에러를 잡았다.
  • 해야 할 일을 나누고 우선순위를 정해서 개발했다.
  • 추후 개발 수정이 필요한 곳은 :FIXME 표시를 하였다.
  • 3 사람이 기능별로 나눠서(ex 나의담화, 담화마켓, 구독 등) 작업하여 컨플릭트가 발생하지 않도록 하였다.
# 큰 그림

1. 라우팅 (기존 React-router-dom → NextJs 라우터 변경)
    - FIXME 남은거 해결하기
2. History (useHistory 삭제)
3. Window (window undefined 에러 삭제)
4. 마이그레이션
5. Fetch (후순위)
    - 컴포넌트 나눠서 같이 개발
    - SSR / CSR 정하기(후순위)
6. 세션
7. SEO(후순위)
8. 광고 스크립트(후순위) 
    - GA
    - 페이스북 픽셀
9. helmet(후순위)
10. next/image(후순위)
11. 404 페이지

 

결론 (NextJs 배포 이후)

  • 마케팅 툴

CRM 데이터라이즈 작업할 때 메타 태그에 user_id 정보를 넣어달라는 요청이 있었는데, 기존에 NextJs를 사용하지 않을 때는 불가능하여 dom에 attribute를 넣는 방식으로 해결했다. 추후 다른 마케팅 툴에서 메타 태그를 이용할 경우 NextJs 포팅 이후에 간편하게 메타 태그를 넣어 마케팅 툴 도입을 더 용이하게 할 수 있게 되었다.

 

  • 카카오톡 미리 보기

기존에는 상품 상세 페이지 링크를 카카오톡으로 보내면 상품 이미지와 상품명이 나오는 게 아니라 회사 로고와 술담화라는 같은 제목으로 나왔는데 nextjs에서 메타 태그 페이지별로 넣어서 카카오톡 미리 보기 화면에 상품명과 상품 썸네일이 나오도록 변경되었다. 일반 사용자에게는 가장 크게 체감되는 좋은 변화였다.

 

성능 비교

  • 데이터독
    • 기존 CRA 관련 데이터가 남아있지 않아 비교 불가
  • fetch가 없는 Static Generation 하는 페이지는 미리 javascript 가 실행된 페이지를 로드하기 때문에 훨씬 더 빨라졌다.
    • Google PageSpeed Insights 사용
    • /damwhaMarket/detail/180 ( 담화박스 페이지 )
    • 용어
      • FCP: 페이지가 로드되기 시작하고 콘텐츠의 일부가 화면에 렌더링 될 때까지의 시간
      • TTI: 웹 페이지가 완전히 상호작용이 가능(interactive)하게 되는 시점
      • TBT: 주 스레드가 input 응답을 막을 정도로 오래 차단되었을 때 FCP와 TTI 사이의 총 시간
      • LCP: 페이지에서 가장 용량이 큰 콘텐츠가 표시되는 시점
    버전/기기 First Contentful Paint Time to Interfactive Speed Index Total Blocking Time Largest ContentFull Paint
    CSR / Mobile 25.0 초 36.1 초 33.2 초 5640 밀리초 36.8초
    SSG / Mobile 5.2초 22.3초 13.9초 2650 밀리초 5.2초
    비교 20초 절감(80% 감소) 14초 절감(38% 감소) 20초 절감(58% 감소) 3000 밀리초 절감(53% 감소) 31초 절감(86% 감소)
    CSR / Desktop 4.4 초 6.7 초 8.1 초 1040 밀리초 5.5 초
    SSG / Desktop 1.2초 4.8초 3.2초 830 밀리초 6.5초
    비교 3초 절감(77% 감소) 2초 절감(28% 감소) 5초 절감(60% 감소) 200 밀리초 절감(20% 감소) 1초 증가(18% 증가)
    • 결론
      • 육안으로 확인되는 페이지 로딩 시간이 약 79% 줄었다.
      • 인터렉션 가능한 상태의 초기 페이지 로딩 시간이 약 33% 줄었다.
      • 이로 인한 페이지 이탈률 감소에 큰 영향을 끼쳤을 것으로 판단된다.
  • ISR로 상품 상세 페이지 로딩 시간을 체감상 확실히 단축했다.
    • SSG와 같음
  • 라이트하우스 (구글에서 개발한 웹 페이지 품질 점수를 매겨주는 도구)

NextJs 도입 이전과 이후 비교

  Performance Accessibility Best Practices SEO
담화마켓 랜딩 모바일 6 → 18 (300% 증가) 81 → 75 (8% 감소) 42 → 58 (38% 증가) 91 → 100 (10% 증가)
담화마켓 랜딩 PC 39 → 45 (15% 증가) 63 → 60 (5% 감소) 58 → 67 (15% 증가) 91 → 100 (10% 증가)
제품 상세페이지 모바일 11 → 29 (263% 증가) 79 → 76 (4% 감소) 50 → 58 (16% 증가) 85 → 100 (18% 증가)
제품 상세페이지 PC 47 → 77 (63% 증가) 77 → 61 (21% 감소) 58 → 67 (15% 증가) 91 → 100 (10% 증가)
이벤트 페이지 모바일 11 → 13 (18% 증가) 62 → 76 (22% 증가) 50 → 58 (16% 증가) 77 → 100 (30% 증가)
이벤트 페이지 PC 42 → 19 (55% 감소) 63 → 75 (19% 증가) 58 → 67 (15% 증가) 91 → 100 (10% 증가)

퍼포먼스 향상이 엄청났다

다만 퍼포먼스 값 자체는 꽤 낮은 곳이 있어서 개선이 필요하다. 큰 이미지를 사용하기 때문에 퍼포먼스가 낮게 나오고 있다.

 

NextJs 남은 과제들

1. vercel 기능들 잘 활용

이를 위하여 Nextjs에 대한 학습이 더 필요. version follow up도 필요

 

2. Next.js 13 버전 적용 필요

 

2. On-Demand-ISR 적용 필요

기존의 ISR처럼 revalidate의 타임에 의존하는 게 아니라 실질적으로 데이터가 변경 시 업데이트(정적 생성)를 시켜준다. 이는 기존의 불필요한 페이지 생성의 단점을 막아준다는 장점이 있다.

 

추후 대규모 프로젝트 진행 시 어떻게 할 것인가

1. 대규모 프로젝트 중에 다른 프로젝트가 들어오는 경우는 지양하자.

NextJs를 개발하는 도중에 매출에 중요한 "프로젝트 A(가칭)"가 들어왔다. 그래서 NextJs 브랜치를 내버려두고 프로젝트 A 브랜치를 따서 2~3주간 개발을 끝낸 뒤 다시 NextJs에 merge 해야 했다. NextJs는 폴더 구조와 라우터 모두 다르기 때문에 merge 하는 게 꽤 큰 일이었다. 그래서 되도록이면 NextJs와 같은 대규모 프로젝트 중에 다른 프로젝트를 개발하는 일은 지양하는 것이 좋다. 물론 현실적으로 회사가 먹고살기 위해 일이 들어온다면 어쩔 수 없이 해야겠지만

 

2. 새로운 프레임워크를 선택할지 말지 어떻게 결정하는 게 좋을까?

결론적으로 NextJs를 도입하여 우리 서비스가 좋아졌지만 처음 도입할 때는 이것을 도입하면 뭐가 좋아지는지 명확히 알지 않고 시작했다.

다음번에는 새로운 프레임워크를 도입했을 때 어떤 이유에서 도입하게 되는지 분석 후 시작하자.

 

공동 집필자

이병수 : https://github.com/ByeongSooLee-KR

 

ByeongSooLee-KR - Overview

ByeongSooLee-KR has 26 repositories available. Follow their code on GitHub.

github.com

정윤기

 

레퍼런스

spa : https://www.huskyhoochu.com/what-is-spa/

라이트하우스 : https://velog.io/@dell_mond/Lighthouse-사용법

 

술담화란?
https://www.sooldamhwa.com/

 

찾아오는 인생술, 술담화

국내 최초 전통주 술 정기 구독 서비스 쇼핑몰 술담화에서 소믈리에가 추천하는 다양한 술을 경험하세요

www.sooldamhwa.com

 

반응형

댓글