React

[React] 글로벌스타일(GlobalStyle) 적용 with Styled-Components

캐럿노트 2024. 6. 25. 01:36

styled-components의  createGlobalStyle을 활용하여 GlobalStyle을 적용하려고 한다.

 

개발환경

  • React 18
  • TypeScript
  • Styled-Components
  • webpack

 

styled-nomalize & styled-reset 설치

npm i styled-normalize
npm i styled-reset
styled-normalize
브라우저 간의 기본 스타일 차이를 줄이면서 일관된 스타일을 제공하는 normalize.css를 CSS-in-JS 환경에서 사용할 수 있게 하는 유틸리티

styled-reset
모든 브라우저의 기본 스타일을 제거하여 일관된 초기 상태를 제공하는 reset.css를 CSS-in-JS 환경에서 사용할 수 있게 하는 유틸리티

 

webpack.config.js 설정

폰트 파일을 번들링하기 위해 Webpack 설정에서 module: rules에 loader 추가하기

{
  test: /\.(woff|woff2|eot|ttf|otf)$/i,
  use: [
    {
      loader: "file-loader",
      options: {
        name: "assets/fonts/[name].[hash:8].[ext]",
      },
    },
  ],
},

 

GlobalStyle.ts 파일 생성

  • assets/fonts 폴더에 Noto Sans woff 파일을 넣어놓고 다음과 같이 font-face를 정의했다.
  • html 또는 body에 font-family를 적용하면 브라우저는 페이지 로딩 시 한 번만 계산하고, 하위 요소들은 그 스타일을 상속받아 사용한다. 이는 전체 선택자(*)를 사용하는 것보다 계산을 덜 필요로 하여 더 효율적이다.
  •  font-face 정의만으로는 폰트 적용이 되지 않고 font-family를 적용해주어야 한다.
import { createGlobalStyle } from "styled-components";
import { normalize } from "styled-normalize";
import { reset } from "styled-reset";
import ThinNotoSansKr from "assets/fonts/NotoSansKR-Thin.woff";
import ExtraLightNotoSansKr from "assets/fonts/NotoSansKR-ExtraLight.woff";
import LightNotoSansKr from "assets/fonts/NotoSansKR-Light.woff";
import RegularNotoSansKr from "assets/fonts/NotoSansKR-Regular.woff";
import MediumNotoSansKr from "assets/fonts/NotoSansKR-Medium.woff";
import SemiBoldNotoSansKr from "assets/fonts/NotoSansKR-SemiBold.woff";
import ExtraBoldNotoSansKr from "assets/fonts/NotoSansKR-ExtraBold.woff";
import BlackNotoSansKr from "assets/fonts/NotoSansKR-Black.woff";

/**
 * 글로벌 스타일
 */
const GlobalStyles = createGlobalStyle`
    ${reset};
    ${normalize};
    * {
        box-sizing: border-box;
        font-family: 'Noto Sans CJK KR'; 
        font-weight: 500;
        letter-spacing: -0.5px;
        font-size: 12px;
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 100;
        src: url(${ThinNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 200;
        src: url(${ExtraLightNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 300;
        src: url(${LightNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 400;
        src: url(${RegularNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 500;
        src: url(${MediumNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 600;
        src: url(${SemiBoldNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 700;
        src: url(${ExtraBoldNotoSansKr}) format('woff');  
    }
    @font-face {
        font-family: 'Noto Sans CJK KR';
        font-style: normal;
        font-weight: 800;
        src: url(${BlackNotoSansKr}) format('woff');  
    }
    body {
        padding: 0;
        margin: 0;
        font-family: 'Noto Sans CJK KR';  // body 태그에 폰트 적용
    }
    button {
        border: none;
        background-color: transparent;
    }
`;

export default GlobalStyles;

 

import 오류 발생

위와 같이 파일을 작성하면 아래와 같이 "모듈 또는 해당 형식 선언을 찾을 수 없습니다"라는 오류가 발생한다.

 

fonts.d.ts 파일 생성

TypeScript에서 .ttf, .woff 파일 등 모듈로 사용될 파일 타입을 선언해 주어야 한다.

// types/fonts.d.ts

declare module "*.ttf" {
  const value: any;
  export = value;
}

declare module "*.otf" {
  const value: any;
  export = value;
}

declare module "*.woff" {
  const value: any;
  export = value;
}

declare module "*.woff2" {
  const value: any;
  export = value;
}

declare module "*.eot" {
  const value: any;
  export = value;
}

 

tsconfig.json 파일 수정

include 부분에 타입 지정한 파일명을 추가해준다.

// tsconfig.json

{
  "compilerOptions": {
    // ...
  },
  "include": ["src", "fonts.d.ts"],
  // ...
}

 

GlobalStyles.ts 파일 적용

Bootstrap.tsx 또는 index.ts, App.tsx 등 최상단 파일에서 GlobalStyles.tsx 파일 호출 및 글로벌 스타일 적용한다.

import { GlobalStyle } from "./styles/GlobalStyle";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <>
    <GlobalStyle />
    <App />
  </>
);

 

특정 파일에서 사용시 따로 파일을 import할 필요 없이 바로 사용 가능하다.

const Header = styled.div`
  font-family: Noto Sans CJK KR;
  font-weight: 500;
  font-size: 12px;
`;

 

문제 발생 - font를 지속적으로 로드

컴포넌트들을 로드할 때마다 fonts들을 로드하며 폰트들이 모두 풀렸다가 다시 Noto Sans가 적용되는 현상이 발생했다.

개발자 도구 → 네트워크 탭에서 폰트를 지속적으로 로드하는 것을 확인할 수 있었다.

 

원인을 몰라 GlobalStyle.ts 파일 설정, webpack의 config, file-loader, 리액트 랜더링등 여러가지를 시도를 했었다.

결국 아래와 같은 원인을 찾을 수 있었다.

 

 Styled-Components는 스타일이 Render 될 때 마다 head 태그의 style 태그를 변경한다. 즉, 새로운 스타일이 등장할 때마다 폰트를 재요청하는 현상이 나타난 것

 

해결방법은 font-face 부분만 별도 css로 분리하는 것이라고 한다.

 

font-face를 css 파일로 분리

/* styles/fonts.css */

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 100;
  src: url("assets/fonts/NotoSansKR-Thin.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 200;
  src: url("assets/fonts/NotoSansKR-ExtraLight.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 300;
  src: url("assets/fonts/NotoSansKR-Light.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 400;
  src: url("assets/fonts/NotoSansKR-Regular.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 500;
  src: url("assets/fonts/NotoSansKR-Medium.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 600;
  src: url("assets/fonts/NotoSansKR-SemiBold.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 700;
  src: url("assets/fonts/NotoSansKR-ExtraBold.woff") format("woff");
}

@font-face {
  font-family: "Noto Sans CJK KR";
  font-style: normal;
  font-weight: 800;
  src: url("assets/fonts/NotoSansKR-Black.woff") format("woff");
}

 

// styles/GlobalStyles.ts

import { createGlobalStyle } from "styled-components";
import { normalize } from "styled-normalize";
import { reset } from "styled-reset";

/**
 * 글로벌 스타일
 * 폰트는 styles/fonts.css에서 관리
 */
const GlobalStyles = createGlobalStyle`
    ${reset};
    ${normalize};
    * {
        box-sizing: border-box;
    }
    html, body{
        font-size: 12px;
        font-weight: 500;
        letter-spacing: -0.5px;
        font-family: 'Noto Sans CJK KR', sans-serif;
        padding: 0;
        margin: 0;
    }
    button {
        border: none;
        background-color: transparent;
    }
`;

export default GlobalStyles;

 

최상단에서 GlobalStyle과 font-face가 정의된 css를 함께 불러서 적용한다.

// Bootstrap.tsx

import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { ToastContainer } from "react-toastify";
import "styles/fonts.css";
import GlobalStyles from "styles/GlobalStyles";
import App from "./App";

const rootElement = document.getElementById("app") as HTMLElement;
const root = createRoot(rootElement);
const queryClient = new QueryClient();

root.render(
  <QueryClientProvider client={queryClient}>
    <GlobalStyles />
    <App />
    <ToastContainer />
  </QueryClientProvider>
);

 

참고자료