본문 바로가기

개발/React

Nextjs와 Vanilla Extract 사용 시 css가 로드되지 않는 문제 Trouble Shooting

반응형

 

 

 

개요

 

회사 프로젝트를 진행하면서 Nextjs 13버전의 Page Router 프로젝트르 Nextjs 14버전으로 올리고 App Router로 마이그레이션해가는 과정(App Router와 Page Router가 혼재하는 상황)에서 Server Component를 사용하기 위해 기존에 사용하던 Emotion을 걷어내고 여러가지 방법 중 Zero Runtime을 보장하는 Vanilla Extract를 적용하고있었다. 그러던 도중 Refresh 시에는 정상적으로 그려지지만 Navigate 이후에는 스타일이 깨져보이는 문제가 발생해 이에 대해 파헤쳐본 것을 간단하게 글로 적어보려고 한다.

 

 

 

본문

 

먼저 정확한 프로젝트 상황은 다음과 같다.

 

  • Nextjs 14 버전 사용
  • @Vanilla-Extract/next-plugin 2.4.6 버전 사용
  • App Router와 Page Router가 혼재

 

 

위 상황에서 정확한 현상

 

  • 처음 실행 후 그려지는 화면에서의 스타일은 문제가 없다.
  • 이후 지면을 이동하면 도착하는 지면에서의 스타일이 깨져있다.
  • 정확히 파악해보면 Sprinkles의 내용은 존재하지만 Style로 선언한 스타일은 누락되어있다.
  • 깨져있던 지면에서 다시 Refresh를 하면 정상적인 스타일로 그려진다.
  • 이 현상은 Development 환경에서만 나타난다. staging, production으로 올라가는 순간 동일한 이슈는 발생하지 않는다.

 

였다. 이러한 문제를 바탕으로 Vanilla Extract의 Issue를 찾아보았다.

 

 

https://github.com/vanilla-extract-css/vanilla-extract/issues/1377

 

Next.js: Styles not loading after navigation with mixed pages/app · Issue #1377 · vanilla-extract-css/vanilla-extract

Describe the bug This is a continuation of #1152 with a new reproduction. Having a project half migrated to the app directory breaks the CSS in the pages in /pages. The code that fixes the CSS load...

github.com

 

https://github.com/vanilla-extract-css/vanilla-extract/issues/1152

 

When developing with Next.js, css is not generated · Issue #1152 · vanilla-extract-css/vanilla-extract

Describe the bug When a page transition is made on the Next.js development server, the css of the destination page is not generated. Reproduced with version 2.2.1 of @vanilla-extract/next-plugin, l...

github.com

 

 

역시 위와 같이 비슷한 이슈를 마주한 사람들이 있었다.

 

 

해당 이슈에 달려있는 내용을 보면 

 

 

 

 

아래에서 유효해보이는 코멘트를 발견했는데 vanilla-extract/next-plugin 에서 next-style-loader가 사라졌기 때문에 문제가 발생했다고 한다.

 

또한 최초 이슈를 제기한 사람의 내용을 보면

 

 

Reproduced with version 2.2.1 of @vanilla-extract/next-plugin, lowering the version to 2.1.3 makes it work correctly.

 

 

위와 같이 2.2.1 버전에서 발생했고, 2.1.3 버전으로 낮췄을때에는 정상적으로 동작했다고 했다.

 

그렇다면 이제 그때 무슨일이 발생했는지를 알아보기 위해 Release Note를 보고 해당 커밋을 확인해봤다.

 

 

https://github.com/vanilla-extract-css/vanilla-extract/pull/1105/files

 

fix(nextjs-plugin): properly handles css loader by SukkaW · Pull Request #1105 · vanilla-extract-css/vanilla-extract

Closes #1101. Closes #1131. Changes: Always choose css-loader/MiniCssExtractPlugin.loader instead of getClientStyle from Next.js (which will opt-in next-style-loader during development mode in the...

github.com

 

 

 

 

여기서 주요하게 볼 부분은 Plugin을 생성해주는 createVanillaExtractPlugin 에 생긴 변화였다.

 

 

1. 기존에는 Nextjs의 Webpack Config에서 CSSRule을 수정하던 로직 앞에 다른 로직이 들어왔다.

 

const findPagesDirResult = findPagesDir(dir, resolvedNextConfig.experimental?.appDir);

// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336
// https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626
// https://github.com/vercel/next.js/pull/43916
const hasAppDir =
  // on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object
  !!resolvedNextConfig.experimental?.appDir && !!(findPagesDirResult && findPagesDirResult.appDir);

const outputCss = hasAppDir
  ? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components
    true
  : // There is no appDir, do not output css on server build
    !isServer;

 

 

위 내용을 살펴보면 아무래도 해당 변화가 있던 시기는 Nextjs 12버전에서 13버전, 즉 App Router가 나타나 이에 대해 대응하기 위함이었던것으로 보인다. 이렇게 나누어야했던 이유는 Server Build 도중에는 Server Component에서 사용하기 위한 css가 필요하지 않기때문에 css에 대한 output이 필요하지 않았기 때문으로 보인다.

 

 

 

2. 기존에 Next에서 제공하는 css-loader를 사용하다가 이를 custom한 css-loader로 교체했다.

 

 

 

여기서 이제 custom한 css loader를 가지고오는 로직을 살펴보자

 

// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7
const getVanillaExtractCssLoaders = (
  options: WebpackConfigContext,
  assetPrefix: string,
) => {
  const loaders: webpack.RuleSetUseItem[] = [];

  // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L14
  if (!options.isServer) {
    // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44
    // next-style-loader will mess up css order in development mode.
    // Next.js appDir doesn't use next-style-loader either.
    // So we always use css-loader here, to simplify things and get proper order of output CSS
    loaders.push({
      loader: NextMiniCssExtractPlugin.loader,
      options: {
        publicPath: `${assetPrefix}/_next/`,
        esModule: false,
      },
    });
  }

  const postcss = () =>
    lazyPostCSS(
      options.dir,
      getSupportedBrowsers(options.dir, options.dev),
      undefined,
    );

  // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28
  loaders.push({
    loader: require.resolve('next/dist/build/webpack/loaders/css-loader/src'),
    options: {
      postcss,
      importLoaders: 1,
      modules: false,
    },
  });

  // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L43
  loaders.push({
    loader: require.resolve(
      'next/dist/build/webpack/loaders/postcss-loader/src',
    ),
    options: {
      postcss,
    },
  });

  return loaders;
};

 

 

내용을 보면 loader에 NextMiniCssExtractPlugin.loader를 활용하는 방식으로 변경된 것을 볼 수 있다.

 

 

NextMiniCssExtractPlugin.loader가 동작하는 방식은 다음과 같다.

 

  1. CSS 파일 별도 추출
  2. HTML에 link 태그로 삽입
  3. 변경사항이 발생하면 새로운 CSS 파일을 생성 및 link 태그의 href를 업데이트하고 브라우저가 새 CSS 파일을 요청

 

 

그러면 이전에 사용하던 getGlobalCssLoader의 내용은 무엇일까? 이는 실제로 내부 구현을 간단하게 보면 아래와 같다.

 

function getGlobalCssLoader(options) {
  if (options.isClient && options.isDevelopment) {
    // 개발 환경 + 클라이언트에서는 next-style-loader 사용
    return {
      loader: 'next-style-loader',
      options: {
        insert: function(element) {
          const anchor = document.querySelector('#__next_css__DO_NOT_USE__');
          anchor.parentNode.insertBefore(element, anchor);
        }
      }
    };
  } else {
    // 그 외의 경우 MiniCssExtractPlugin.loader 사용
    return {
      loader: MiniCssExtractPlugin.loader
    };
  }
}

 

 

 

결론적으로는 원래 style-loader를 활용하던 방식에서 이를 MiniCssExtractPlugin.loader로 변경하게되어 문제가 발생한 것으로 보였다.

 

 

따라서 현재 프로젝트에서 해결한 방식은 다음과 같다.

 

 

webpack: (config, { dev, isServer }) => {
    const cssRules = config.module.rules.find((rule) => Array.isArray(rule.oneOf)).oneOf;

    cssRules.forEach((rule) => {
      if (rule.test?.test?.('.vanilla.css')) {
        if (dev && !isServer) {
          if (Array.isArray(rule.use)) {
            rule.use = rule.use.map((loader) => {
              if (typeof loader === 'object' && loader.loader?.includes('mini-css-extract-plugin')) {
                return {
                  loader: require.resolve('style-loader'),
                  options: {
                    injectType: 'singletonStyleTag',
                    // insert 옵션을 문자열로 변경
                    insert: '#__next_css__DO_NOT_USE__',
                    attributes: {
                      'data-vanilla-extract': '',
                    },
                  },
                };
              }
              return loader;
            });
          } else if (typeof rule.use === 'object') {
            if (rule.use.loader?.includes('mini-css-extract-plugin')) {
              rule.use = {
                loader: require.resolve('style-loader'),
                options: {
                  injectType: 'singletonStyleTag',
                  insert: '#__next_css__DO_NOT_USE__',
                  attributes: {
                    'data-vanilla-extract': '',
                  },
                },
              };
            }
          }
        }
      }
    });

    return config;
  },

 

 

 

위의 로직을 next.config.js의 webpack에 넣었는데, 이것으로 해결하고자 한 것은 다음과 같다.

 

1. 기존에 mini-css-extract-plugin으로 처리하던 것을 style-loader로 다시 교체했다.

2. 그 과정에서 nextjs에서 권장하는 next_css_DO_NOT_USE를 기준으로 스타일 태그를 정렬하도록 했다.

 

 

 

위와 같은 설정 이후에는 정상적으로 동작했다.

 

 

결론

 

간단하게는 @Vanilla-Extract/next-plugin을 내리면 되는 문제기도 했다. 하지만 위의 문제는 App Router로의 전환 이후에는 유효하지 않았고, 현재 상황에서 App Router로의 전환 이후에 라이브러리 버전 변경 없이 진행하고 싶은 마음에 소스코드를 뒤적여보게 되었고, 실제로 어떤 loader를 사용하여 변환했고, 어떻게 하고자 했었는지를 알게 되어서 의미있는 트러블 슈팅이었다.

반응형