[React] 글로벌 폰트 적용 - 폰트 깜빡이는 이슈 해결

Minsu·2024년 8월 27일
2

트러블슈팅

목록 보기
4/4

부트캠프에서 진행한 팀 프로젝트 이후 노션에만 정리해뒀던 트러블슈팅 글을 벨로그로 옮겨왔다. 글을 정리하며 다시 한번 당시 상황을 되짚어볼 수 있었다. 🙂

문제 상황

로컬에 저장한 웹 폰트를 글로벌 폰트로 적용 시 렌더링 될 때마다 폰트가 튀는? 현상이 발생했다. (폰트가 뒤늦게 적용되는 듯한 현상)

적용 코드

// GlobalStyle.ts

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`

@font-face {
  font-family: 'Pretendard';
  font-weight: 100;

  src:
      url('/Pretendard-Thin.otf') format('opentype'),
      url('/Pretendard-Thin.woff') format('woff'),
      url('/Pretendard-Thin.ttf') format('truetype');
}
...
`;

export default GlobalStyle;
// App.tsx

import React from 'react';
import { Reset } from 'styled-reset';
import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import router from './routes/routes';
import store from './app/store';
import GlobalStyle from './styles/\bGlobalStyle';

function App() {
    const persistor = persistStore(store);

    return (
        <>
            <Reset />
            <GlobalStyle />
            <Provider store={store}>
                <PersistGate loading={null} persistor={persistor}>
                    <RouterProvider router={router} />
                </PersistGate>
            </Provider>
        </>
    );
}
export default App;

원인 추론 & 해결 과정


해당 현상의 원인 파악 및 해결이 당장 어려워 당시 적용된 폰트를 제거하였고, 제거 후 폰트 튀는 현상 없이 정상 동작하는 것을 확인할 수 있었다. 하지만 적용하려고 했던 폰트의 적용이 안된 상태이기 때문에 원하는 폰트를 적용하면서 해당 문제를 해결할 수 있는 방법을 모색해보았다.

브라우저 네트워크를 살펴보니 처음 페이지 로딩 이후 컴포넌트가 렌더링될 때마다 계속해서 폰트가 불어와지고 있는 것을 확인할 수 있었다.

1차 시도

font-display 속성을 이용하여 폰트 로딩 처리

검색을 하다가 FOIT, FOUT이라는 것에 대해 알게되었고, FOUT으로 인한 문제일까 싶어 font-display 속성을 이용해 해당 현상을 보다 막아보고자 했다.

FOIT? FOUT??

  • FOIT(Flash of Invisible Text) - 브라우저가 폰트를 다운로드 하기 전까지는 글자가 보이지 않음.
  • FOUT(Flash Of Unstyled Text) - 브라우저가 폰트를 다운로드 하기 전까지 폰트가 적용되지 않음.

→ 브라우저가 콘텐츠를 화면에 그리는 동안 폰트 리소스 응답이 늦어지면 FOIT, FOUT 이 발생한다.

먼저, 브라우저가 렌더링될 때의 동작 원리를 알아보자.

브라우저가 렌더링될 때 정보를 요청하는 순서

브라우저의 렌더링 과정이다.

T0: 브라우저가 HTML 문서를 요청.
T1: HTML 응답이 오면 DOM을 그리고 , CSS/JS 및 기타 리소스를 요청.
T2: 모든 CSS 콘텐츠를 수신하여 CSSOM을 만들고 CSSOM을 DOM트리와 결합하여 렌더링 트리를 구성.
T3: 콘텐츠를 화면에 그리고 이 때 폰트를 사용할 수 없는 상태라면 FOIT, FOUT 발생.

font-display 속성 이용

  • auto - 브라우저의 기본 동작. 대부분의 브라우저는 FOIT을 선호한다.
  • block - 웹 폰트가 로드될 때까지 텍스트를 노출하지 않음.
  • swap - 웹 폰트가 로드될 때까지 대체 글꼴을 사용하여 텍스트를 표시함.
  • fallback - swap + block 버전. 아주 짧은 시간(~100ms)동안 텍스트를 숨기고, 이 이후에도 폰트 로딩이 완료되지 않으면 swap과 같이 대체 폰트를 표시함.
  • optional - fallback과 비슷하지만, 아주 짧은 시간(~100ms)의 폰트 로딩 시간을 제공하고, 그 이후에도 교체되지 않음.

나는 이중 font-display: block; 을 사용하여 FOUT 대신 FOIT 방식으로 로딩되도록 했다.

그러나..! 폰트가 튀는 대신 화면이 깜빡거리는 것처럼 보이는 부자연스러운 결과가 도출되었고, 덧붙여 계속해서 폰트를 불러오는 것과 같은 근본적인 부분을 해결해주지 못했다.

2차 시도

개발자 도구로 살펴보니 style 태그가 변경될 때마다 폰트가 재요청되어 불러와지고 있는 것을 발견할 수 있었다.

찾아보니 해당 현상은 styled-components가 가지고 있는 자체적인 특성이었다. (🔗참고Link)

위에서 언급했듯이 styled-components의 createGlobalStyle을 사용하여 style을 초기화하고 폰트를 정의했는데, styled-components는 새로운 스타일이 렌더될 때마다 <style> 태그를 변경하고, <style> 태그 안에 폰트 정의가 함께 있어 새로운 스타일이 등장할 때마다 font를 재요청하는 것이다. 이는 폰트가 깜빡거리는 문제를 야기하고, 때문에 유저들이 styled-components 측에 폰트 스타일 정의를 분리해달라고 요청했으나 반영되지 않은 것으로 확인했다.

해당 프로젝트에서 styled-components를 사용했기 때문에, 시도해볼 수 있는 방법은 아래 두 가지였다.

  1. 폰트를 css로 정의하여 APP.tsx 파일에 import하기
  2. CDN 링크 삽입

"둘 다 해보자!"

1. 폰트를 css로 정의하여 APP.tsx 파일에 import하기

createGlobalStyle 내에서 폰트 정의 시 렌더링될 때마다 폰트를 재요청하는 이슈가 있으므로, 해당 폰트 정의 부분을 별도 css 파일을 생성해 분리했다. 이후 App.tsx에 폰트를 정의한 CSS 파일을 import하여 적용해줬다.

적용 코드

//src>fonts>Font.css

@font-face {
  font-family: 'Pretendard';
  font-weight: 100;

  src:
      url('./Pretendard-Thin.otf') format('opentype'),
      url('./Pretendard-Thin.woff') format('woff'),
      url('./Pretendard-Thin.ttf') format('truetype');
}

@font-face {
  font-family: 'Pretendard';
  font-weight: 200;

  src:
      url('./Pretendard-ExtraLight.otf') format('opentype'),
      url('./Pretendard-ExtraLight.woff') format('woff'),
      url('./Pretendard-ExtraLight.ttf') format('truetype');
}

...
// App.tsx

import React from 'react';
import { Reset } from 'styled-reset';
import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import router from './routes/routes';
import GlobalStyle from './styles/globalStyle';
import './fonts/Font.css';


import store from './app/store';

function App() {
    const persistor = persistStore(store);

    return (
        <>
            <Reset />
            <GlobalStyle />
            <Provider store={store}>
                <PersistGate loading={null} persistor={persistor}>
                    <RouterProvider router={router} />
                </PersistGate>
            </Provider>
        </>
    );
}
export default App;

이후 GlobalStyle 내에서 body의 font-family를 프리텐다드로 설정했다.

2. CDN 링크 삽입 (CDN 웹 폰트 적용)

CDN?

Contents Delivery Network을 의미하며, 실제 사용하려는 리소스를 직접 다운로드 및 첨부 등의 과정을 생략 후 제공하는 서버에서 실시간으로 바로 사용할수있게 거리를 줄이는 것을 의미한다.

프리텐다드가 제공하는 CDN 링크를 넣어보았다.

// index.html
  
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css" />

출처: https://github.com/orioncactus/pretendard

결과 관찰

결과적으로 CSS 파일로 정의하여 import하는 방법과 CDN 링크를 삽입하는 방법 모두 폰트를 재요청하는 이슈를 해결할 수 있었다.

다만, CDN을 통해 폰트를 불러올 경우 CDN 서버와의 연결 상태에 따라 폰트 로딩 속도가 지연될 수 있으므로 폰트를 css로 정의하여 APP.tsx 파일에 import하는 방법을 채택했다.

시연 영상시연 영상

→ 리렌더링 시에도 폰트를 재요청하지 않고, 폰트가 깜빡이는 현상 없이 정상적으로 동작하는 것을 확인했다.

참고페이지

profile
지식의 해상도를 높여가는 개발자 (っ'-')🖥️

0개의 댓글

관련 채용 정보