끄적이는 개발노트

Next.js 15 - Styled-Components 본문

JavaScript/Next.js

Next.js 15 - Styled-Components

크런키스틱 2025. 4. 23. 22:07
728x90

Next.js 에서 css를 다루는 방법에는 기본적인 built-in 방식과 Styled Component, TailwindCSS 방식이 대표적일 것이다.

물론, Next.js에서는 기본적으로 create app을 할 때부터 TailwindCSS 적용 옵션을 설정하면 기본 세팅을 해줄 정도로 가장 편리하고 공식적으로 추천하는 방법이다. (본인 역시 TailwindCSS를 선호한다.)

 

하지만, Styled-Components 역시 아직까지 꽤나 사용되는 방법이기에 적용 방법을 정리해둔다.

 

1. 작동 방식

Styled-Components는 css-in-js 방식으로 작동한다.

CSS-in-JS?
Javascript 코드 내에 css를 생성하는 로직이 담기는 방식이다.
그렇기 때문에 변수에 접근이 가능하여 props와 같은 형태로 dynamic하게 스타일을 적용할 수 있다.
하지만, 미리 모든 스타일을 적용하기 때문에 렌더링이 더 오래걸릴 수도 있다.

 

이렇게 보면 별 문제는 없어보이지만, Next.js는 기본적으로 SSR 형식이다. 또한, 15버전에 들어오면서 server component로 이루어져있는데 이와 호환성이 굉장히 떨어진다. Styled-Components는 클라이언트가 런타임일 때, styledsheet를 생성하고 DOM에 주입한다. 따라서 SSR 방식에서 동작하게 되면 서버에서 HTML이 미리 생성되는데 style은 런타임  때 생성되고 주입되기 때문에 깜빡임이 발생한다. 그렇기 때문에 공식문서에서도 CSS-in-JS 방식에 대해 경고를 하고 있다.

 

 

2. 해결을 위한 설정

next.config.js

// next.config.js

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  compiler: {
    styledComponents: true,
  },
};

export default nextConfig;
  • SWC 를 통해 활성화
SWC (Speedy Web Compiler)
Rust로 제작된 매우 빠른 자바스크립트 컴파일러
JS/TS 트랜스파일/컴파일

 

 

src/lib/registry.tsx

src 폴더 아래에 lib/registry.tsx 파일을 생성하고 아래와 같이 작성해준다.

// registry.tsx

"use client";

import React, { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  // Only create stylesheet once with lazy initial state
  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== "undefined") return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

위는 공식문서 코드로 useServerInsertedHTML hook을 통해 렌더링 시 생성된 Styled-Components의 스타일을 수집하고 head에 삽입하는 역할을 한다.

 

 

layout.tsx

// layout.tsx

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import StyledComponentsRegistry from "@/lib/registry";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

root layout 파일에 생성한 style registry로 wrapping 한다.

 

이에 대한 전체적인 과정을 공식문서에서 아래와 같이 설명하고 있다.

알아두면 좋은 정보

- 서버 렌더링 중에 스타일은 전역 레지스트리로 추출되어 <head>HTML에 반영됩니다. 이를 통해 스타일 규칙이 해당 규칙을 사용하는 콘텐츠보다 먼저 배치됩니다. 향후 React에서 추가될 기능을 사용하여 스타일을 어디에 삽입할지 결정할 수 있습니다.

- 스트리밍 중에 각 청크의 스타일이 수집되어 기존 스타일에 추가됩니다. 클라이언트 측 하이드레이션이 완료되면 styled-components평소처럼 작업을 이어받아 추가적인 동적 스타일을 주입합니다.

- 스타일 레지스트리 트리의 최상위 레벨에 클라이언트 컴포넌트를 사용하는 이유는 CSS 규칙을 추출하는 것이 더 효율적이기 때문입니다. 이렇게 하면 후속 서버 렌더링 시 스타일을 다시 생성할 필요가 없고, 서버 컴포넌트 페이로드에 스타일이 포함되는 것을 방지할 수 있습니다.

- 스타일이 적용된 구성 요소 컴파일의 개별 속성을 구성해야 하는 고급 사용 사례의 경우 Next.js 스타일이 적용된 구성 요소 API 참조를 읽어 자세히 알아보세요.

 

 

3. 코드

styles 폴더에 styledComponents.ts 파일을 생성한다.

// styledComponent.ts

import styled, { css } from "styled-components";

export const Wrapper = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #000000;
`;

export const Title = styled.h1`
  font-size: 64px;
  color: white;
`;

export const TestDiv = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 200px;
  background-color: gray;
  margin: 4px;
  padding: 4px;
`;

export const Title2 = styled.p<{ $large?: boolean }>`
  ${(props) =>
    props.$large
      ? css`
          color: white;
          font-size: 32px;
        `
      : css`
          color: black;
          font-size: 20px;
        `}

  ${TestDiv}:hover & {
    color: orange;
  }
`;

export const TestDiv2 = styled(TestDiv)`
  background-color: blue;
`;

 

가볍게 살펴보자면,

  • 방식은 기본 styled 선언과 동일
  • styled 객체와 함께 custom하고자 하는 tag를 수정(Tagged Templated Literal 방식)
  • props를 통해 dynamic style 설정
    • 각각의 css 값을 props(ex. width, height)로 변경
    • 위와 같이 임의의 변수를 받고 그 값에 따라 여러 css를 한번에 변경 가능
  • 백틱 안에서 : &와 같은 기호와 같은 가상선택자 사용 가능
  • styled 객체에서 위에 선언한 component를 상속받아서도 사용 가능

 

// page.tsx

"use client";

import {
  TestDiv,
  TestDiv2,
  Title,
  Title2,
  Wrapper,
} from "@/styles/styledComponent";

export default function Home() {
  return (
    <Wrapper>
      <Title>hi</Title>
      <TestDiv>
        <Title2 $large>test</Title2>
      </TestDiv>
      <TestDiv2>
        <Title2>test</Title2>
      </TestDiv2>
    </Wrapper>
  );
}

 

"use client"를 추가해 클라이언트 컴포넌트임을 명시해야 한다.

 

4. 실행

아래와 같이 잘 실행되는 것을 확인할 수 있다.

다만, TailwindCSS가 따로 class명과 사용방식에 있어 러닝커브가 있고 코드가 다소 지저분해지더라도 TailwindCSS가 성능적으로나 사용면에서나 훨씬 편리하고 효율적인 것 같다...

728x90