Next.js에서 Tailwind CSS, Cypress 사용기

2021년 8월 30일
Template Library

Template Library 시리즈의 두 번째 글을 작성하고자 합니다. 이번에 시도한 내용은 계속 사용해오던 Next.js입니다. Next.js 및 이 글에서 사용한 라이브러리들이 무엇인지는 이미 많은 글이 함께하고 있으니 간략하게만 이야기하고 넘어가고자 합니다.

필자는 근 몇 년간 React와 Next.js를 실무에서 많이 사용하고 있습니다. 여기에 Ant Design까지 덧붙여서 세트로 사용하고 있습니다. 필자가 작성하고 있는 대부분의 프로젝트는 이 세트를 베이스로 다른 라이브러리들을 선택하여 시작합니다. 그래서 Template Library를 시작할 때 기회가 생기면 지금까지 사용하던 방식을 돌아보고 정리하고자 하는 생각을 계속 가지고 있었습니다.

이번에 정리하면서 언제든 다른 것으로 대치하는 것을 고려할 수 있는 Ant Design을 제외하고 대신 BPL(Banksalad Product Language) 무야호라는 공유에서 보았던 Tailwind CSS를 사용하였습니다. 늘 마음속에 함께하지만 이런저런 이유로 프론트에서는 시도하지 못했던 테스트도 함께 고민하였습니다.

하기에 대한 내용은 이 저장소에서 확인하실 수 있습니다.

Next.js

Next.js는 SSR 및 Static 환경을 제공해주는 프레임워크입니다. 필자는 앞서 언급했듯이 이 프레임워크를 다양한 프로젝트에서 사용하고 있습니다. 그래서 사실 Next.js 자체에 대한 부분은 코드를 정리하고 나중에 참고하고자 하는 정도로 생각하였습니다. Next.js에 대한 설치는 공식 문서의 Getting Started를 참고하시면 어렵지않게 설정하실 수 있고, 많은 글이 있어 참고하기도 쉬운 프레임워크입니다.

전반적인 구성은 클라이언트는 이전과 동일하게 Atomic Design을 사용하였지만, 이번에는 서버 구성을 조금 달리하였습니다.

.
├── src
│   ├── client
│   ├── pages
│   └── server
└── package.json

기존에 자주 사용하던 구성은 다음과 같습니다. 당연히 별도의 API 서버를 구성하겠지만 외부로 주소나 접근을 노출하고 싶지 않은 경우나 부가적인 행위를 넣기 위해 프록시 성격의 서버를 별도로 구성하였습니다. 문제는 별도의 서버를 구성하고 이 또한 TypeScript를 적용하고자 하니 아래와 같이 별도로 코드에 대한 변경 감지나 빌드 과정이 필요하게 되었습니다. 별도의 빌드가 추가되어 업데이트 등의 유지보수가 부담스럽고 서버 쪽 코드를 수정하면 전체적으로 재시작하게 되어 생산성에도 좋지 못하다고 늘 생각하고 있었습니다.

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon --watch \"src/server/**\" --ext \"ts,json\" --exec \"ts-node --project tsconfig.server.json src/server/index.ts\"",
    "build:server": "cross-env NODE_ENV=production tsc --project tsconfig.server.json",
    "build:next": "cross-env NODE_ENV=production next build",
    "build": "npm run build:next && npm run build:server",
    "start": "cross-env NODE_ENV=production node dist/index.js",
  }
}

이번에 API Routes를 사용하여 해당 부분에 적용하여 확인해보고자 하였습니다. 이번 프로젝트의 특성상 추가적인 재처리는 필요하지 않고 단순하게 프록시 용도로만 사용하고자 하였기 때문에 프록시는 next-http-proxy-middleware를 사용하여 처리하였습니다.

// src/pages/api/[...all].ts
import { NextApiRequest, NextApiResponse } from 'next';
import httpProxyMiddleware from 'next-http-proxy-middleware';

export default (req: NextApiRequest, res: NextApiResponse): Promise<unknown> =>
  httpProxyMiddleware(req, res, {
    target: 'http://localhost:8080',
    pathRewrite: {
      '^/api': '/api',
    },
  });

전체 API로 전환 및 프록시 처리는 별도의 어려운 과정 없이 위와 같이 간단하게 추가하여 처리할 수 있었습니다.

{
   "scripts": {
    "build": "next build",
    "dev": "next dev",
    "start": "next start",
  }
}
.
├── src
│   ├── api
│   ├── components
│   ├── constants
│   ├── decorators
│   ├── interfaces
│   ├── pages
│   ├── stores
│   ├── styles
│   └── utils
└── package.json

이전 구성과 달리 Next.js 기본 설정에서 별도의 추가적인 설정 없이 사용할 수 있었습니다. 디렉터리 구조에서도 Server를 제거하고 단순하게 사용할 수 있었습니다.

Tailwind CSS

Tailwind CSS는 Utility-First 컨셉을 가진 CSS 프레임워크로 부트스트랩을 사용해봤다면 익숙하게 느껴질 flex, pt-4, text-center class와 같은 도구를 제공합니다. 자세한 내용은 Hello Tailwind CSS! | 장점, 단점, 사용법 및 다른 글들을 참고하면 좋을 것 같습니다. 필자도 간단하게 해당 컨셉만 확인하고 공식 문서를 참고해가며 적용해보았는데, 큰 어려움 없이 사용할 수 있었습니다.

Next.js에 설정하는 방법은 공식 문서에서도 잘 설명되어있었습니다만, 필자의 경우는 Styled Component도 사용하기 위해 Tailwind CSS와 다른 라이브러리를 적절하게 섞어주는 twin.macro도 함께 사용하였습니다. 이에 대한 설정은 개발환경 구축[Next+Emotion+Tailwind+Twin.macro] 및 다른 글을 참고하시면 어렵지 않게 설정하실 수 있을 것 같습니다.

const Avatar: FC<PropsType> = props => {
  const { url, size, ...wrapperProps } = props;
  let width: number;

  switch (size) {
    case 'small':
      width = 5;
      break;
    case 'large':
      width = 32;
      break;
    case 'middle':
    default:
      width = 8;
      break;
  }

  return (
    <Wrapper {...wrapperProps}>
      <Container className={`w-${width} h-${width}`}>
        <Img className="rounded-full" src={url ?? DEFAULT_IMAGE} alt="avatar" />
      </Container>
    </Wrapper>
  );
};

export default Avatar;

const Wrapper = styled.div`
  ${tw`flex flex-row justify-center`}
`;

const Container = styled.div`
  ${tw`relative flex justify-center items-center rounded-full`}
`;

const Img = styled.img`
  ${tw`rounded-full`}
`;

Tailwind를 사용하기 전에는 class가 길어져 정확하고 더 보기 힘들지 않을까 걱정을 했었는데, 막상 적용하고 보니 CSS로 작성했을 때보다 간결하게 작성할 수 있어서 전반적인 생산성이 좋아진 것 같습니다. Styled Component를 같이 사용하면 적절한 타협을 하며 꽤 만족스럽게 쓸 수 있었습니다. 필자가 만약 Ant Design 같이 별도의 디자인 시스템을 사용하지 않거나 디자인 시스템을 구축해야 하는 상황이라면 채택하여 사용할 것 같습니다. 당장 이 블로그를 갈아타고 싶지만 할 것이 너무 많네요.

Cypress

Cypress는 브라우저에서 동작하는 E2E 테스트 도구입니다. 쉽게 테스트를 작성할 수 있는 도구를 제공해주며, 브라우저에서 동작하기 때문에 실제 테스트가 동작하는 것을 보며 테스트를 작성하기에 정말 편하고 생산성이 좋았습니다. Headless로 테스트를 수행할 수 있으며 이에 대한 작업을 동영상으로도 제공하여 확인하기도 좋습니다.

전반적인 테스트에 대한 설정은 Next.js 공식 문서 Jest and React Testing Library에서 확인하실 수 있습니다.

다만 한 가지 어려웠던 부분은 getServerSideProps 등의 SSR을 위해 서버에서 API를 호출하는 경우, 이를 Mocking 하는 부분이었습니다.

Browser 및 Node에서 호출을 통해 Mock Server를 구축할 수 있는 Mockttp를 사용하여 이를 해결하였습니다. 이에 대한 설정은 cypress-mockttp 저장소를 참고하여 설정하였습니다. 이 설정을 해도 실제 실행하면 Cypress가 실행하면서 새로 고침 되고 이로 인해 테스트하고자 하는 포트를 사용할 수 없는 문제가 발생하게 됩니다. 이는 아래와 같이 Mock Server를 실행할 때 해당 포트를 사용하고 있으면 해당 포트를 강제로 종료하고 생성하도록 추가하는 Command를 만들어 사용하였습니다.

import 'cypress-file-upload';
import { getRemote } from 'mockttp';

const mockServer = getRemote();

Cypress.Commands.add('mockServerStart', port => {
  return mockServerStart(port);
});

Cypress.Commands.add('mockServerStop', () => {
  return mockServerEnd();
});

Cypress.Commands.add('mockServerBuilder', (method, url, response, statusCode) => {
  return mockServerBuilder(method, url, response, statusCode);
});

async function mockServerStart(port = 8080) {
  try {
    return await mockServer.start(port);
  } catch (e) {
    mockServer.mockServerConfig = { port: 8080 };
    await mockServer.requestFromMockServer('/stop', {
      method: 'POST',
    });
    mockServer.mockServerConfig = null;
    return mockServer.start(port);
  }
}

function mockServerEnd() {
  return mockServer.stop();
}

function mockServerBuilder(method, url, response, statusCode = 200) {
  return mockServer[method](url).thenReply(statusCode, JSON.stringify(response));
}

작성에 큰 어려움이 없고, 테스트의 수행을 한 눈에 보기도 좋아서, 한바탕 개발이 끝난 뒤에 작성하거나 필요하다면 코드 단위로 검증하며 작성해 나가기도 좋을 것 같다는 생각이 들었습니다.

TestRenderer & React Testing Library

앞서 서두에서 Cypress만 언급하였지만 간단하게나마 TestRendererReact Testing Library도 사용하였습니다. TestRenderer는 Snapshot을 위해, React Testing Library는 단위 테스트를 위하여 사용했습니다. 두 라이브러리 모두 앞서 언급하였던 Jest and React Testing Library를 통해 쉽게 설정할 수 있었습니다.

간단하게나마 작성해보았을 때 React Testing Library는 단위 테스트를 하는 데 큰 어려움 없이 할 수 있어서 좋았습니다. 다만 지금까지의 필자의 개발 환경에서는 오픈소스로 제공되고 있는 디자인 시스템을 사용하고 있으며, 단위 컴포넌트별로 검증하기에는 디자인의 변경 및 일정 등으로 쉽지 않아서 당장은 사용이 어려울 것 같았습니다.

TestRenderer의 경우는 간단하게 주어진 Mock 데이터를 바탕으로 UI를 구성하여 변경 점을 볼 수 있어, 필자의 개발 환경에서는 일차로 개발이 끝난 뒤에 적용해보면 좋을 것 같다고 생각되었습니다.

마치며

짧다면 짧고 길다면 긴 대략 3~4주의 기간 동안, 전반적으로 기존 환경에 대해서 정리해볼 수 있었습니다. 지금까지 관성적으로 사용해보던 부분을 지나, 호기심의 영역이자 부담스러웠던 영역을 한번 경험해서 해소할 수 있었습니다. 다음에 이를 바탕으로 새로운 프로젝트를 구성할 때 참고하여 큰 도움이 될 것 같습니다.

Recently posts
© 2016-2023 smilecat.dev