[NextJS] 이커머스 사례분석 및 스토리북 연동

선영·2023년 5월 12일
1

📚 Library

목록 보기
8/14
post-thumbnail

🧨 문제점


넥스트js를 사용하면 페이지가 바뀔때마다 서버단에서 데이터를 pre-rendering한 뒤, 리액트 하이드레이션으로 데이터를 올린다. 그런데 헤더, 푸터, 햄버거 메뉴같이 공통으로 사용되는 컴포넌트의 경우 페이지가 바뀌어도 데이터의 변동이 없고 고정되어야 한다. 그렇기 때문에 이런 부분은 페이지가 아니라 레이어로 구현할 필요가 있다. 현재는 앱이 페이지마다 공통 컴포넌트의 데이터를 포함한 전체 데이터를 다 받아야하는 문제가 있으므로 구조적인 문제를 해결해야했다.

📚 E-commerce Best Practice 분석


기존의 통합 이커머스 플랫폼에서 NextJS를 사용하여 헤드리스 플랫폼으로 전환하는 사례이자, 대규모 이커머스 사이트를 재구축하는 팁이다.

여기서 헤드리스 플랫폼이란, 사용자에게 보여지는 프론트엔드 화면과 백엔드를 분리하는 것을 말한다.

여기서 잠깐, nextJS로 이커머스를 구축하는 이점은 무엇일까? 해당 사례에서 기존의 사이트에 사용했던 플랫폼은 ASP.NET이라는 서버 웹 프레임워크 기반이었고, 방문자가 많아지면서 사용자와의 더 많은 상호작용을 위해 React와 같은 클라이언트 웹 프레임워크 개념을 혼합한 솔루션을 구축했다. 하지만 라이브 서비스를 시작하면서 트래픽이 많아지자 성능 문제가 발생했다. 핵심 웹 바이탈(CWV)은 이커머스에서 더욱 중요하다. 실제로 여러 사이트의 데이터를 분석했을때 0.1초의 성능 개선이 전환율 10%증가로 이어질 수 있다.

여기서 핵심 웹 바이탈(CWV)은 세 가지 웹 성능 지표의 집합이다.
구글 검색엔진에서는 이 세가지 지표를 측정하여 검색 결과에 표시할 페이지를 결정하는데 반영한다.
즉, 검색 엔진 최적화(SEO)실무자는 해당 페이지의 순위를 높이기 위한 전체적인 전략의 일환으로
웹 페이지의 핵심 웹 바이탈을 최적화해야 한다.

CWV는 다음과 같다.
 - 로딩 속도를 측정하는 최대로 의미 있는 페인트(LCP)
 - 페이지 상호 작용성을 측정하는 첫 번째 입력 지연(FID)
 - 시각적 안정성을 측정하는 누적 레이아웃 변경(CLS)

참고 | https://www.cloudflare.com/ko-kr/learning/performance/what-are-core-web-vitals/

이런 성능 문제를 완화하기 위해 예산이 책정되지 않은 많은 서버를 추가하거나, 역방향 프록시에서 페이지를 적극적으로 캐시하거나, 심지어는 사이트 기능의 일부를 비활성화해야만 한다. 결국 일부 페이지만 정적으로 제공하는 매우 복잡하고 비용이 많이드는 솔루션을 사용하게 되는 것이다.

여기서 역방향 프록시란 원본 서버 앞에 위치하며 어떤 클라이언트도 원본 서버와 직접 통신하지 못하도록 한다.
자체 역방향 프록시를 구축하기 위해선 집중적인 소프트웨어 및 하드웨어 엔지니어링 리소스와 물리적 하드웨어에
대하여 상당한 투자가 필요하다. 역방향 프록시의 모든 이점을 얻는 가장 쉽고 비용 효율적인 방법 중 하나는
CDN서비스에 가입하는 것이다.

참고 | https://www.cloudflare.com/ko-kr/learning/cdn/glossary/reverse-proxy/

그런 부분에 있어서 nextJS는 페이지를 정적으로 생성할 수 있는 React기반 웹 프레임워크지만, 서버 측 렌더링도 사용할 수 있어 이커머스에 이상적이다. Vercel이나 Netilfy같은 CDN에서 호스팅할 수 있으므로 지연시간도 짧고, 서버측 렌더링에 서버리스 기능을 사용하므로 가장 효율적으로 확장할 수 있다.

하지만 nextJS로 개발하면서 생산성에 너무 집중하다 보면 코드의 유지보수를 소홀히 할 수 있기 때문에 위험하기도 하다. 시간이 지남에 따라 이런 부분과 js만의 유형화되지 않은 특성은 코드베이스의 성능 저하로 이어질 수 있다. 즉, 버그의 수가 증가하고 생산성이 떨어지게된다.

런타임 측면에서도 문제가 될 수 있다. 코드의 아주 작은 변경으로 인해 성능 및 기타 핵심 웹 바이탈이 저하될 수 있다. 또한 서버 측 렌더링을 부주의하게 사용하면 예상치 못한 서비스 비용이 발생할 수 있다.

위와 같은 문제점들을 극복하면서 얻는 교훈은 다음과 같다.

코드베이스 모듈화

먼저, npx create-next-app을 실행하면 다음과 같은 폴더 구조를 갖게 된다. (대부분의 예제에서도 이와 같은 구조로 되어 있다.) 아래와 같은 구조에 더 큰 컴포넌트를 위해 components폴더에 몇 개의 하위 폴더를 만들고, 대부분의 컴포넌트는 루트 구성 요소 폴더에 있었다. 이 접근 방식은 소규모 프로젝트에서는 괜찮다. 하지만 프로젝트가 커지면서 컴포넌트와 컴포넌트가 어디서 사용되는지 추론하기 어려워질 수 있다. 또한 어떤 코드가 어떤 코드에 종속되어야 하는지에 대한 명확한 지침이 없기 때문에 혼란스럽기도 하다.

/public
  logo.gif
/src
  /lib
    /hooks
      useForm.js
  /api
     content.js
  /components
     Header.js
     Layout.js
  /pages
     Index.js

위와 같은 문제를 해결하기 위해 코드베이스를 리팩터링하고 기술 개념이 아닌 기능 모듈(NPM모듈과 같은 종류) 별로 코드를 그룹화하기로 결정했다. 아래의 예시에는 체크아웃 모듈과 카탈로그 모듈이 있다. 이런 방식으로 코드를 그룹화하면 폴더 구조만 보고도 코드베이스에 어떤 기능이 있고 어디에서 찾을 수 있는지 정확히 알 수 있으므로 검색이 더 쉬워진다. 또한 종속성을 추론하기가 훨씬 쉬워진다. 이전 상황에선 컴포넌트 간에 많은 종속성이 있었다. 그래서 체크아웃의 변경 사항에 대한 풀리퀘스트가 카탈로그의 구성 요소에도 영향을 미쳤다. 이로 인해 병합 충돌 횟수가 증가하고 해당 모듈을 변경하기가 더 어려워졌다.

/src
  /modules 
    /catalog
      /components
        productblock.js
    /checkout
      /api
        cartservice.js
      /components
        cart.js

가장 효과적인 해결책은 모듈 간의 종속성을 최소한으로 유지하고(종속성이 꼭 필요한 경우 단방향인지 확인) 모든 것을 하나로 묶는 ‘프로젝트’레벨을 도입하는 것이었다. 아래의 ‘프로젝트’레벨은 레이아웃 및 템플릿에 대한 코드가 포함되어 있다. nextJS에서 페이지 컴포넌트는 하나의 규칙이고 이는 실제 페이지로 이어진다. 이런 페이지는 동일한 구현을 재사용해야 하는 경우가 많기 때문에 ‘페이지 템플릿’이라는 개념을 도입했다. 예를 들어 제품 상세 페이지 템플릿은 카탈로그의 구성요소를 사용하여 제품 정보를 표시하고, 결제 모듈의 카트에 추가 구성 요소도 사용한다. 또한 기능 모듈에서 재사용해야 하는 일부 코드가 있기 때문에 공통 모듈도 있다. 여기엔 일관된 모양과 느낌을 제공하는 데 사용되는 react 컴포넌트인 간단한 원자들이 포함되어 있다. 또한 인프라 코드도 포함되어 있는데, 특정 일반 react 훅이나 GraphQL 클라이언트 코드를 생각하면 된다.

/src
  /modules
    /common
      /atoms
      /lib 
    /catalog
      /components
        productblock.js
    /checkout
      /api
        cartservice.js
      /components
        cart.js
    /search
  /project
    /layout
      /components
    /templates
      productdetail.js
      cart.js
  /pages
    cart.js

아래는 위 솔루션의 시각적인 개요이다.

마이크로 프론트엔드

더 큰 솔루션이나 여러 팀과 함께 작업하는 경우에는 애플리케이션을 소위 마이크로 프론트엔드로 더 많이 분할하는 것이 합리적일 수 있다. 즉, 다음 예시와 같이 애플리케이션을 서로 다른 url에서 독립적으로 호스팅되는 여러 개의 물리적 애플리케이션으로 더 많이 분할하는 것을 의미한다. 그런 다음 프록시 역할을 하는 다른 애플리케이션에 의해 통합된다.

여기서 프록시 서버란 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 주는
컴퓨터 시스템이나 응용 프로그램을 가리킨다.

nextJS의 ‘재작성 기능’은 이를 위해 매우 유용하며, 소위 멀티 존에서 이러한 기능을 지원한다. 다중 영역의 장점은 모든 영역이 자체 종속성을 관리한다는 것이다. 또한 코드베이스를 점진적으로 발전시키는 것이 더 쉬워진다. nextJS 또는 react의 새 버전이 출시되면 전체 코드베이스를 한 번에 업그레이드 할 필요 없이 구역을 하나씩 업그레이드할 수 있다. 여러 팀으로 구성된 조직에선 팀 간의 종속성을 크게 줄일 수 있다.

TypeScript의 사용

컴포넌트가 수정되고 리팩터링되면서 일부 컴포넌트 props가 더 이상 사용되지 않는 것을 발견할 수 있다. 또한 컴포넌트에 전달되는 프로퍼티의 유형이 누락되거나 잘못되어 버그가 발생할 수도 있다. 타입스크립트는 자바스크립트의 상위 집합으로, 컴파일러가 코드를 정적으로 검사할 수 있는 타입을 추가한다.

nextJS는 타입스크립트를 통해 솔루션에 점진적으로 추가할 수 있다. 즉, 전체 코드베이스를 한 번에 다시 작성하거나 변환할 필요 없이 바로 사용을 시작하고, 나머지 코드베이스를 천천히 변환할 수 있다.


🎆 StoryBook 연동


스토리북은 ui컴포넌트와 page빌드 툴이다. 즉, ui컴포넌트와 page를 앱(웹)이랑 분리해서 따로따로 개발할 수 있도록 돕는 툴이다. 결국 ui컴포넌트 빌딩, 테스트, documendation의 기능을 제공해준다. 즉, 다음과 같은 컴포넌트 드리븐 방식의 개발이 가능해진다. 참고로 프로젝트에서 컴포넌트를 만들 때 스토리북으로 export시켜서 컴포넌트 별로 테스트하고 관리하는 방식이다.

하지만 nextJS가 더 발전하면서 next/image와 같은 기능이 추가되면서 스토리북과 같은 문서 및 테스트 환경과 통합하기 더 어려워지는 부분이 있다. 스토리북을 nextJS 페이지를 위한 최상의 컴포넌트 드리븐 UI 환경으로 만드는 방법을 자세히 알아보자. 이 문서에선 다음과 같은 4단계와 동일한 작업을 수행하는 방법을 보여준다.

  • Webpack5로 새 스토리북 초기화
  • nextJS 페이지용 스토리 만들기
  • preview.js에서 공유된 전역 스타일 가져오기
  • Next Image 최적화를 해제하여 스토리에서 사용하기

Webpack 5로 새 스토리북 초기화

nextJS v11이상은 웹팩5를 사용한다. 또한 스토리북에서 웹팩5를 사용하여 향상된 통합 및 성능을 얻을 수 있다. 이를 위해 npx sb init --builder webpack5 명령어를 실행한다.

전체 nextJS 페이지에 대한 스토리 만들기

nextJS와 스토리북은 매우 호환되는 컴포넌트 모델을 갖고 있다. nextJS는 페이지 컴포넌트를 사용하고 스토리북은 문서화 및 테스트 컴포넌트를 사용한다. 이건 스토리북을 nextJS를 위한 훌륭한 컴포넌트 드리븐 개발 환경을 만들어준다. 다음과 같은 순서로 nextJS 홈페이지에 대한 스토리를 만들 수 있다.

  1. /stories/pages/home.stories.jsx 경로에 새 파일 만들기
  2. Import /pages/index.js
  3. title 및 component 속성을 사용해서 기본 스토리 객체 내보내기
  4. Home에 스토리 내보내기
// /stories/pages/home.stories.jsx

import Home from "../../pages/index";

export default {
  title: "Pages/Home",
  component: Home,
};

export const HomePage = () => <Home />

여기까지 하면 스토리북에 아래와 같이 홈페이지가 추가된다. 하지만 아직은 별로 볼 것은 없다. 전역 스타일을 가져와야 한다.

preview.js에서 공유된 전역 스타일시트 가져오기

대부분의 앱에는 전역 리셋을 위한 스타일시트나 전역적으로 공유되는 글꼴 스타일이 있다. 지금의 nextJS앱의 경우엔 /styles/globals.css 의 경로에 있다고 가정한다.

글로벌 스타일시트를 home.stories.jsx스토리 파일로 가져올 수 있다. 그러나 그렇게 하려면 스토리 파일에 많은 중복이 필요하며 오류가 발생하기 쉽다. 때문에 모든 스토리에 대해 하나의 글로벌 스타일시트를 가져오는 것이 좋다.

스토리북은 .storybook/preview.js의 경로에서 공유된 스토리 구성을 포함하고 있다. 이 파일은 모든 스토리가 렌더링되는 방식을 제어한다. 스타일시트는 모듈 가져오기로 가져올 수 있다.

// .storybook/preview.js
import "../styles/globals.css";

결과적으로 아래와 같이 스토리에 globals.css가 추가되면서 스토리북에 추가된 홈페이지가 훨씬 보기 좋아졌다! 하지만 여전히 홈페이지 하단부의 이미지가 깨져보인다.

스토리의 nextJS 이미지 최적화 해제

nextJS와 스토리북 통합에서 가장 어려운 부분은 이미지 처리이다. 스토리북은 /public경로에서 next/images를 제공하도록 구성되어야 한다.

nextJS v10이상 부터 Image 컴포넌트를 사용할 수 있고, 이는 “현대 웹을 위해 진화한 html <img>요소의 확장”이다. 이를 활용하면 파일 크기, 시각적 안정성 및 로드 시간이 최적화된다.

이와 같이 nextJS는 image size optimization를 사용하는 반면에 스토리북에서는 이걸 못하기 때문에 몇가지의 설정이 필요하다.

  1. /public경로에서 nextJS 이미지를 제공

    sb init명령어를 치면 package.json에 두 개의 스토리북 스크립트가 생성된다. 이 스크립트들을 실행하면, /public경로(nextJS 이미지가 보관되는 위치)를 제공한다.

    // package.json
    
    "scripts": {
    -    "storybook": "start-storybook -p 6006",
    -    "build-storybook": "build-storybook"
    +    "storybook": "start-storybook -p 6006 -s ./public",
    +    "build-storybook": "build-storybook -s public"
    }
  2. 스토리북에서 사용되는 next/image모듈의 optimization을 끄기

    nextJS 이미지 컴포넌트가 사용되는 모든 곳에서 이미지는 /_next-prefixed경로에서 제공된다. nextJS dev server가 실행중이지 않을때 NextImage의 props API와 속성을 활용하고 싶다면 unoptimized props를 사용하면 된다. 스토리북에서 이것을 설정하기 위해 아래와 같이 모듈을 설정해주면 스토리에서만 nextJS이미지 최적화를 해제할 수 있다.

    // .storybook/preview.js
    import * as NextImage from "next/image";
    
    const OriginalNextImage = NextImage.default;
    
    Object.defineProperty(NextImage, "default", {
      configurable: true,
      value: (props) => <OriginalNextImage {...props} unoptimized />,
    });
    
    // 위 코드의 적용이 실패하는 경우,
    import * as NextImage from "next/image";
    
    NextImage.defaultProps = {
      unoptimized: true,
    };

    위 코드 스니펫은 스토리북이 next/image모듈을 평가하는 방법을 수정한다. 위와 같이 설정하게 되면 nextJS Image의 default export가 사용되는 모든 곳에서 unoptimized props가 적용된다. 서버를 다시 시작하면 svg이미지가 깨지지 않고 잘 나오는 것을 확인할 수 있을 것이다.

스토리북에서 getServerSideProps() 테스트

getServerSideProps()를 사용하면 서버에서 컴포넌트의 props를 준비하는데 ui툴인 스토리북에선 이게 되지 않는다. 하지만 이걸 사용하지 않으면 완벽한 기능 테스트가 어렵기 때문에 팀원간에 miscommunication이 발생하게 될 수 있다.

아래와 같이 업데이트 해주면 서버에서 얻은 name을 Home컴포넌트에서 보이게 할 수 있다. 즉, http://localhost:3000/api/hello에서 얻어온 특정 이름이 보이게 된다. 하지만 스토리북에는 변화가 없다. 왜냐하면 위에서 말했듯이 ui툴인 스토리북이 nextJS의 server-side props를 수행하지 않기 때문이다.

//index.js
export const getServerSideProps = async () => {
  const res = await fetch("http://localhost:3000/api/hello");
  const data = await res.json();
  if (!data) {
    return {
      notFound: true,
    };
  }
  return {
    props: data,
  };
};

const Home: NextPage<{ name: string }> = ({ name }) => {
...
	Welcome to <a href="https://nextjs.org">Next.js! {name}</a>
...

스토리북에서도 getServerSideProps()를 수행한 것 같은 기능을 넣어주려면 아래와 같이 업데이트 해준다. Home컴포넌트에 args를 넘겨주도록 하고, HomePage.args = { name: "John Doe" } 와 같이 기본값을 전달해주면 된다.

// /stories/pages/home.stories.jsx
import { ComponentStory } from "@storybook/react";
import Home from "../../pages/index";

export default {
  title: "Pages/Home",
  component: Home,
};

export const HomePage: ComponentStory<typeof Home> = (args) => (
  <Home {...args} />
);
HomePage.args = { name: "John Doe" }; //default args

아래와 같이 스토리북에서 name을 변경할 수 있게 되며, 디자이너나 다른 팀원들이 getServerSideProps()를 한 것 처럼 평가 및 테스트를 할 수 있게 된다.

☑️ 참고


profile
Superduper-India

0개의 댓글