Nextjs13 twin.macro 도입

MyeonghoonNam·2023년 5월 10일
2

시작하며

기존 emotion을 통한 CSS-in-JS 방식에서 단순한 컴포넌트의 HTML 코드 보다 스타일 코드가 더 복잡한 경우를 경험하였다.

스타일 코드가 실제 기능 코드의 가독성을 해친다고 생각하여 tailwindCSS를 사용하게 되었다.
emotion을 사용하며 불편했던 문제들을 다소 해소할 수 있었고 마크업 작성도 보다 쉽게 할 수 있었지만 불편한점을 발견하였다.

바로 동적 스타일링이였다.

가변적인 변수를 받아와 스타일을 적용해야하는 경우 tailwindCSS는 한계가 있었다. 이러한 트러블 슈팅을 해결하기 위해 방안을 찾아보던중 twin.macro에 대해 알게 되었고, twin.macro는 tailwindCSS와 emotion을 중첩적으로 유연하게 사용가능하게 해주었다.

나의 프로젝트에 twin.macro를 도입하여 적용한 과정을 기록해보려고 한다.
(기존 나의 프로젝트 환경은 nextjs 13 + typescript + tailwindCSS가 이미 구성되어있으므로 이를 인지하자.)

twin.macro Setup

필요한 package들을 설치한다.

npm i @emotion/react @emotion/styled
npm i -S @emotion/serialize
npm i -D twin.macro @emotion/babel-plugin babel-plugin-macros @babel/plugin-syntax-typescript @babel/preset-react

최상위 루트에 withTwin.js 파일을 생성한다.

// withTwin.js

/* eslint-disable no-param-reassign */
const path = require('path');

const includedDirs = [path.resolve(__dirname, 'src')];

module.exports = function withTwin(nextConfig) {
  return {
    ...nextConfig,
    webpack(config, options) {
      const { dev, isServer } = options;
      config.module = config.module || {};
      config.module.rules = config.module.rules || [];
      config.module.rules.push({
        test: /\.(tsx|ts)$/,
        include: includedDirs,
        use: [
          options.defaultLoaders.babel,
          {
            loader: 'babel-loader',
            options: {
              sourceMaps: dev,
              presets: [
                [
                  '@babel/preset-react',
                  { runtime: 'automatic', importSource: '@emotion/react' },
                ],
              ],
              plugins: [
                require.resolve('babel-plugin-macros'),
                require.resolve('@emotion/babel-plugin'),
                [
                  require.resolve('@babel/plugin-syntax-typescript'),
                  { isTSX: true },
                ],
              ],
            },
          },
        ],
      });

      if (!isServer) {
        config.resolve.fallback = {
          ...(config.resolve.fallback || {}),
          fs: false,
          module: false,
          path: false,
          os: false,
          crypto: false,
        };
      }

      if (typeof nextConfig.webpack === 'function') {
        return nextConfig.webpack(config, options);
      }
      return config;
    },
  };
};

그 후에 최상위 루트의 next.config.js 설정 파일에 다음과 같이 webpack 설정을 적용해준다.

const withTwin = require('./withTwin');

/** @type {import('next').NextConfig} */
const nextConfig = withTwin({
  reactStrictMode: true,
});

module.exports = nextConfig;

이제 twin.macro의 type 선언을 위한 파일을 구성해야한다. src/types 디렉토리를 구성하고 twin.d.ts파일을 생성한다.

import 'twin.macro';
import { css as cssImport } from '@emotion/react';
import styledImport from '@emotion/styled';
import CSSInterpolation from '@emotion/serialize';

declare module 'twin.macro' {
  const styled: typeof styledImport;
  const css: typeof cssImport;
}

declare module 'react' {
  interface DOMAttributes<T> {
    tw?: string;
    css?: CSSInterpolation;
  }
}

이러한 type 선언에 대해 살펴보도록 tsconfig.json파일에 types 속성을 추가한다.

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"],
  "types": ["types"] <===== type 선언 추가
}

적용

tw 함수로 기존 tailwindCSS 문법을 적용시키고 css 함수를 통해 emotion의 동적 스타일링을 함께 적용할 수 있게되었다.

import { useState } from 'react';
import tw, { css } from 'twin.macro';

const Home = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <h2>max value === 3</h2>
      <span
        css={[
          tw`text-[36px] block`,
          css`
            color: ${value === 3 && 'hotpink'};
          `,
        ]}
      >
        {value}
      </span>
      <button
        type="button"
        onClick={() => setValue((prev) => prev + 1)}
        className="text-[36px] bolder-[1px]"
      >
        +
      </button>

      <button
        type="button"
        onClick={() => setValue((prev) => prev - 1)}
        className="text-[36px]"
      >
        -
      </button>
    </div>
  );
};

export default Home;

적용방법에 따른 전체코드는 링크를 참고하자.

참고문서

twin.macro

profile
꾸준히 성장하는 개발자를 목표로 합니다.

0개의 댓글