현재 진행중인 프로젝트의 공수 일정을 맞추기 위해 기능 구현만 신경쓴 나머지 코드 품질 개선에는 많은 힘을 쓰지 못했습니다. 특히 style 관련 코드가 매우 난잡하게 작성되어 있는 상황입니다. 저희 프로젝트에선 inline-style이 많으며 styled-component도 사용하지만 적극적으로 쓰이진 않았습니다. 무분별한 inline-style을 사용했고 체계 없는 style 코드를 작성했더라도 지금까지는 치명적인 문제를 입지 않았지만, 이 상태가 지속된다면 여러 방면에서 잠재적인 문제가 하나둘씩 발생할 것입니다. 이를 해결하기 위해 여러 방안을 찾아보았는데요, 본 포스트에선 그 중 scss에 대한 개념Next.js에서 scss가 반영되기까지의 과정을 보여드리겠습니다.

scss란?

 scss(sassy css)란, css의 기능을 확장한 css 전처리기입니다. css 전처리기란, 자신만의 특별한 구문을 가지고 css를 생성하는 프로그램입니다.

 컴퓨터 프로그래머들이 처음으로 css를 접하면 프로그래밍적인 요소가 없어서 많이 당황한다고 합니다. 실제로 css에서 선택자와 그에 적용할 속성들을 작성하다보면 금새 코드가 길어집니다. 따라서, css를 프로그래밍하듯이 작성할 수 있도록 scss가 탄생했습니다. scss는 변수와 nesting을 지원합니다.

variables

$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
  font: 100% $font-stack;
  color: $primary-color;
}

nesting

nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  li { display: inline-block; }

  a {
    display: block;
    padding: 6px 12px;
    text-decoration: none;
  }
}

 scss는 거듭 발전해나가면서 현재는 함수, 제어문, 믹스인, 상속, 그리고 모듈 기능까지도 제공합니다.

module

// _base.scss
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
  font: 100% $font-stack;
  color: $primary-color;
}

// styles.scss
@use 'base';

.inverse {
  background-color: base.$primary-color;
  color: white;
}

scss 컴파일러와 Next.js

그림 출처: https://www.mugo.ca/Blog/7-benefits-of-using-SASS-over-conventional-CSS

 그러나, 이 scss를 브라우저가 직접 읽을 수 없습니다. 결국엔 브라우저가 파싱할 수 있는 css로 컴파일하는 과정이 필요하죠. 대표적으로 node-sass, gulpwebpack이 scss를 css로 컴파일해줍니다. sassmeister에서는 scss를 작성하면 컴파일된 css 결과물을 확인해볼 수 있습니다.

 제 프로젝트에선 Next.js를 사용하는데 scss를 도입하려면 별도의 컴파일러 설정을 해야할 것 같습니다. 하지만, 다행히도 Next.js 공식 문서에 따르면 기본적으로 sass를 지원하고 있습니다! 단순히 sass만 설치하여 쉽게 next.js 프로젝트에 도입해볼 수 있습니다. 혹시라도 sass 컴파일러를 커스터마이징 할 필요가 있다면 next.config.jssassOptions을 수정하라는 안내가 있습니다.

const path = require('path')
 
module.exports = {
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
}

 추가적으로, next.js는 scss에서 선언한 변수를 js 파일 내에서 쓸 수 있는 유용한 기능을 지원한다고 합니다.

// variables.module.scss
$primary-color: #64ff00;
 
:export {
  primaryColor: $primary-color;
}

 export할 때 js에서 카멜 케이스로 쓸 수 있게끔 도와주고 있습니다.

// app/page.js
// maps to root `/` URL
 
import variables from './variables.module.scss'
 
export default function Page() {
  return <h1 style=>Hello, Next.js!</h1>
}

Next.js 프로젝트에서 scss를 화면에 반영하기까지의 과정

 이제 scss가 어떤 과정을 거쳐서 브라우저 화면에 보이는지 알아봅시다.

참고로 저는 scss module을 사용하였습니다. scss moudle을 사용하면 자동적으로 고유한 클래스명을 가지게 만들어 마치 지역 scope을 가진 것 처럼 css 코드를 작성할 수 있습니다. 따라서 css 선택자 클래스명이 중복되는 것을 신경쓰지 않아도 됩니다.

1. Production 환경

 먼저 next.js를 배포한 url로 접속하여 production 환경에서의 과정을 살펴봅시다.

 메인 페이지에 접속하고 개발자도구의 네트워크 탭을 확인해보면 HTML, CSS와 수많은 JavaScript 파일이 응답된 것을 확인할 수 있습니다.

prod_req_1

 아시다시피 Next.js는 초기 로딩시 미리 빌드된 HTML과 CSS파일을 클라이언트에게 보내줍니다. Pre-rendering 기법을 사용하여 사용자에게 최대한 빨리 UI의 구성을 보여주기 위함이죠. 응답된 HTML를 보면 link태그로 css를 불러오고 있습니다.

<html>
    <head>
        <!-- ... -->
        <link rel="preload" href="/_next/static/css/6e406d3509560102.css" as="style"/>
        <link rel="stylesheet" href="/_next/static/css/6e406d3509560102.css" data-n-g=""/>
        <!-- ... -->
    </head>
    <!-- ... -->
</html>

 한 가지 생소한 것은 link 태그의 rel 속성의 preload값 입니다. 이것은 서버로부터 매우 많은 자원들을 불러오는 과정 속에서 가장 우선적으로 불러와야 할 것을 명시적으로 지정해주는 것입니다. 그렇다고해서 불러온 파일을 가장 먼저 실행시켜주는 것은 아니고 단지 먼저 다운로드 되고 먼저 캐싱되어야 할 파일로 스케줄링 해줍니다. Next.js의 기본 설정이 css를 파일을 preload하는 것을 보면 초기 로딩시 JavaScript보다 CSS를 먼저 불러와 우선 정적인 화면을 빠르게 보여주는 것을 목적으로 둔다는 것을 알 수 있습니다.

 실제로 css파일이 가장 먼저 요청되고 있습니다. prod_req_2

 요청으로 온 해당 css 파일을 확인해보겠습니다.

prod_css

 빽빽이 쓰여있고 가독성이 매우 낮네요.. 이는 Next.js가 파일 안에서 코드를 실행하는데 필요없는 주석이나, 공백등을 제거하는 Minifying 과정을 거쳤기 때문입니다. 이를 통해 파일 사이즈를 최대한 줄여 application의 성능을 최대한 높여줍니다.

 그렇다면, css가 응답으로 오기 전에 scss를 css로 컴파일하는 것은 무엇이 담당할까요? 바로 next build때 사용되는 webpack입니다. 자세히는 webpack의 sass-loader가 scss를 css로 컴파일합니다. create-react-app을 실행하면 자동으로 sass-loader모듈이 webpack 설정에 추가되는데 next.js 역시 sass-loader를 사용하고 있습니다.

 최종적으로 bundling과정을 거쳐 프로젝트 여러 곳에 존재하는 css 파일들을 하나의 큰 번들 파일로 묶어서 초기 로딩시 클라이언트에게 주는 것이죠.

 이를 확인하기 위해 scss를 사용하고 있는 다른 페이지에 접속해봅시다.

prod_req_3

 이상하게도 해당 페이지에 필요한 별개의 css 파일을 요청하고 있습니다. 왜 초기 로딩시 받아온 css 번들에 포함되지 않고 추가적으로 css 파일을 요청하고 있는 걸까요?

 이것은 Next.js가 Code-Splitting 기법을 사용하기 때문입니다. 초기 로딩시 프로젝트에 필요한 모든 파일을 번들링한 JavaScript, CSS를 보내게 되면 클라이언트에서 초기 로딩 때 소모되는 시간이 늘어나게 됩니다. 따라서, 초기에 필요한 파일을 번들로 한꺼번에 주는 것이 아니라 번들을 chunk로 작게 나누어 전달하는 것입니다. 이때, chunk는 그것이 요구되는 entry-point(URL)에 접속할 때 전달됩니다.

 Next.js는 기본적으로 chunk를 나누는 기준을 pages/로 두고 있습니다. 예를 들어, pages/posts/1, pages/mypage에 접속했을 때 응답으로 오는 chunk 파일이 각각 다릅니다.

 이때까지의 과정을 요약한 모식도입니다. pages/example 폴더 안의 scss 파일들이 하나의 chunk로 만들어지기 까지의 과정입니다.

모식도1

위 그림에서 주의해야 할 점은 변환 과정이 반드시 해당 순서로 진행되지 않는다는 점입니다. 빌드 과정 동안 위 과정이 종종 concurrently하게 실행되기도 합니다.

2. Development 환경

 이제 development 환경도 확인해봅시다.

dev_req_1

 이상하게도 production때와 달리 네트워크 탭에 css파일이 보이지 않습니다. 응답으로 온 HTML을 확인해도 css 파일을 불러오는 태그가 보이지 않습니다. css를 사용중인 다른 페이지에 접속해도 JavaScript chunk 파일만 올 뿐, css파일은 없습니다. 하지만, 화면을 보면 css가 제대로 적용되어 있습니다. 요소 탭을 들어가 자세히 확인해봅시다.

dev_html

 확인해보니 html의 style태그에 css코드가 들어가있습니다! 응답으로 온 html과 클라이언트에서 확인한 html에 차이가 있는데요, 왜 이럴까요?

 빌드 타임에 미리 컴파일되어 있는 css를 쓰는 production 모드와 달리 development에서는 런타임에서 js를 실행하여 html에 style 태그로 css를 주입합니다. dev 모드에서 이런 방식을 사용하는 이유는 Fast Refresh를 위해서 입니다. 저희가 scss를 수정할 때 마다 compiling, minifying, bundling을 수행하기엔 비용이 많이 소모되므로 그 대신에 코드를 수정할 때 마다 런타임 환경에서 JavaScript를 실행하여 스타일링을 html에 빠르게 적용하기 위함이죠.

 이를 직접 테스트해볼 수 있는 개발자 도구의 유용한 기능이 있습니다. cmd + shift + P를 눌러 JavaScript 사용 중지한다면, 개발 환경에서는 css 스타일링이 입혀지지 않지만, production 환경에선 css 스타일링이 적용되어 있습니다.

disable_js

 CRA의 기본 웹팩 설정을 참고하면, 실제로 위 내용과 같게 구현되어 있음을 알 수 있습니다. isEnvDevelopment인 경우엔 style-loader를 사용하고 있습니다. style-loader는 js에서 css-loader사용해 웹팩 의존성 트리에 추가한 css string값들을 로 넣어줍니다.

  // common function to get style loaders
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      isEnvDevelopment && require.resolve('style-loader'),
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        // css is located in `static/css`, use '../../' to locate index.html folder
        // in production `paths.publicUrlOrPath` can be a relative path
        options: paths.publicUrlOrPath.startsWith('.')
          ? { publicPath: '../../' }
          : {},
      },
      // ...
    ]
    // ...
  }

마치며

 지금까지 scss에 대해서 간단히 알아보았고, scss 파일이 스타일링으로 적용되기 까지의 과정 중 production과 development 환경에 따라 어떤 차이가 있는지 공부해보았습니다. 결론적으로 이를 도와주는 핵심 도구는 webpack입니다. 이때까진 편하게 CRA를 사용해오면서 내부적으로 어떤 일이 벌어지는지 잘 알지 못했는데, 그만큼 webpack이 핵심적인 기능을 담당한다는 것과 webpack의 중요성을 체감할 수 있었습니다.

Reference