웹 서핑을 하다가 기사나 상품 페이지를 읽는 도중, 상단 이미지가 뒤늦게 다운로드되면서 화면 전체가 덜컹 내려앉아 엉뚱한 버튼을 잘못 클릭해 본 경험이 다들 한 번쯤 있으실 겁니다.

저 역시 초기 웹 프로젝트를 배포한 뒤 모바일 환경에서 테스트를 하다가 동일한 문제를 겪었습니다. 텍스트가 먼저 렌더링 된 후 용량이 큰 배너 이미지가 0.5초 뒤에 늦게 로딩되면서, 사용자가 클릭하려던 '장바구니 담기' 버튼이 아래로 쑥 밀려 내려가 '구매 취소' 버튼을 누르게 만드는 치명적인 UI 버그가 발생한 것입니다.

구글 라이트하우스(Lighthouse) 성능 점수를 측정해 보니 CLS(Cumulative Layout Shift, 누적 레이아웃 이동) 점수가 빨간색 경고(0.35 이상)를 기록하고 있었습니다.

오늘은 화면 깜빡임과 덜컹거림을 완벽히 잡고 부드러운 사용자 경험을 만들어 준 CLS 개선 노하우와 최신 CSS aspect-ratio 및 스켈레톤 UI 적용법을 공유해 보려고 합니다.

1. 내가 마주친 문제: "왜 화면이 덜컹거리며 밀려날까?"

브라우저가 HTML을 파싱할 때, 이미지 태그에 가로/세로 크기 정보가 미리 지정되어 있지 않으면 브라우저는 해당 이미지의 크기를 0 x 0 픽셀로 간주하고 아래 요소들을 먼저 위로 바짝 끌어올려 배치합니다.

HTML
 
<!-- ❌ 브라우저가 이미지 크기를 미리 알 수 없어 CLS를 유발하는 위험한 태그 -->
<img src="https://example.com/banner.jpg" alt="이벤트 배너" />

그 후 네트워크를 통해 실제 이미지 파일이 다운로드되면, 브라우저는 그제야 이미지의 진짜 높이(예: 400px)를 인식하고 아래에 있던 콘텐츠들을 강제로 밀어내는 재배치(Reflow / Relayout) 작업을 수행합니다. 이것이 바로 레이아웃 덜컹거림(Layout Shift)의 원인입니다.

2. 해결 방법 1: width와 height 속성으로 종횡비(Aspect Ratio) 미리 알려주기

가장 기본적이면서 강력한 해결법은 HTML <img> 태그에 반드시 원본 이미지의 width와 height 속성을 명시해 주는 것입니다.

HTML
 
<!-- ⭕ 브라우저가 이미지 비율을 미리 계산하여 공간을 확보함 -->
<img 
  src="https://example.com/banner.jpg" 
  alt="이벤트 배너" 
  width="800" 
  height="450" 
  style="width: 100%; height: auto;" 
/>

최신 브라우저는 태그에 적힌 width와 height 값을 기반으로 이미지가 로딩되기 전부터 종횡비(Aspect Ratio: 16/9)를 미리 계산해 정확한 높이의 빈 공간을 미리 예약해 둡니다. 따라서 이미지가 늦게 다운로드되어도 주변 레이아웃이 1픽셀도 흔들리지 않습니다.

3. 해결 방법 2: 최신 CSS aspect-ratio 속성 활용하기

동적으로 불러오는 외부 이미지나 반응형 카드 컴포넌트에는 최신 CSS 속성인 aspect-ratio를 사용하면 훨씬 유연하게 공간을 확보할 수 있습니다.

CSS
 
/* ⭕ 이미지가 다운로드되기 전에도 16:9 비율 공간을 강제로 유지 */
.banner-image-container {
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: #1e293b; /* 로딩 전 빈 공간에 어두운 배경색 지정 */
  border-radius: 12px;
  overflow: hidden;
}

.banner-image-container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

과거에 padding-top: 56.25% 같은 복잡하고 난해한 핵(Hack)을 쓰던 방식에서 벗어나, 단 한 줄의 CSS로 완벽한 반응형 종횡비 공간을 예약할 수 있게 되었습니다.

📸 [사진 첨부 가이드 1]

여기에 VS Code에서 aspect-ratio: 16 / 9; 및 이미지 컨테이너 CSS 코드가 작성된 에디터 스크린샷을 넣으세요!

(캡처 팁: CSS 속성 중 aspect-ratio와 object-fit: cover 부분이 한눈에 들어오도록 에디터 화면을 스크린샷 찍어 첨부해 주세요.)

이미지 컨테이너 css

4. 해결 방법 3: 스켈레톤 UI(Skeleton)로 로딩 체감 속도 극대화

API 통신으로 데이터를 받아오는 동안에는 빈 화면 대신 스켈레톤(Skeleton UI)을 띄워 공간을 유지하면서 사용자에게 "데이터를 불러오는 중"이라는 시각적 피드백을 주는 것이 좋습니다.

CSS
 
/* 은은하게 반짝이는 스켈레톤 애니메이션 */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

.skeleton-box {
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: #334155;
  animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  border-radius: 8px;
}

스켈레톤 UI를 적용하면 레이아웃 밀림 방지는 물론, 사용자가 대기 시간을 훨씬 짧게 체감하게 만드는 효과가 있습니다.

이미지 로드 전 스켈레톤
이미지 로드 후 상태

5. 결론 및 느낀 점

구글 검색 엔진은 이제 단순히 텍스트 내용뿐만 아니라 사용자가 화면을 조작할 때 시각적 안정성을 유지하는지(Core Web Vitals)를 중요한 랭킹 요소로 평가합니다.

  1. 모든 <img> 태그에 종횡비 정보(width/height 또는 CSS aspect-ratio)를 명시할 것
  2. 동적 콘텐츠 로딩 구간에는 미리 높이를 확보한 스켈레톤 UI를 배치할 것

이 두 가지 원칙만 지켜도 화면이 덜컹거리는 불쾌한 사용자 경험을 없애고 웹 사이트의 전문성과 완성도를 한 단계 끌어올릴 수 있습니다!

최근 진행한 프론트엔드 프로젝트의 UI 스타일링 도구로 요즘 생태계에서 가장 핫한 Tailwind CSS를 도입해 보았습니다.

이전까지는 Plain CSS, SCSS, 혹은 CSS Modules 방식을 주력으로 사용하며 클래스명을 정의하고 스타일을 입혀왔습니다. 하지만 프로젝트의 규모가 조금씩 커질수록 CSS 파일 용량이 불어나고, 클래스 이름 지정(Naming)에 소비되는 시간이 늘어나는 등 여러 한계를 체감하게 되었습니다.

오늘은 유틸리티 우선(Utility-First) CSS 프레임워크인 Tailwind CSS를 실제 프로젝트에 직접 도입해 보면서 느꼈던 솔직한 장단점과, 가독성 단점을 극복했던 나만의 리팩토링 노하우를 깊이 있게 공유해 보려고 합니다.

1. 기존 CSS 방식의 한계: "클래스 이름 짓다가 하루가 다 가네요"

CSS를 작성할 때 개발자를 가장 괴롭히는 것 중 하나는 바로 BEM(Block Element Modifier) 커스텀 컨벤션에 맞추어 클래스명을 짓는 작업입니다.

.card__article-title--highlighted처럼 클래스명을 길게 정의하다 보면 "이 이름이 과연 이 컴포넌트의 역할을 명확히 설명하고 있는가?"에 대한 소모적인 고민을 하게 됩니다. 또한, 스타일에 아주 작은 수정사항(예: 마진 4px 추가)이 생겨도 JSX 파일과 CSS/SCSS 파일 사이를 번갈아 이동해야 하는 작업 전환(Context Switching) 비효율이 존재했습니다.

Tailwind CSS는 이러한 페인 포인트(Pain Point)를 직관적으로 해결해 주는 도구였습니다. 클래스명을 고민할 필요 없이, HTML/JSX 태그의 className 속성에 미리 정의된 유틸리티 클래스(flex, items-center, p-4, bg-blue-500 등)를 조합하여 즉시 스타일을 부여하는 방식이기 때문입니다.

2. 프로젝트에서 체감한 Tailwind CSS의 장점

① 개발 생산성 및 작업 속도의 획기적 향상

컴포넌트 로직이 담긴 JSX/TSX 파일 하나에서 마크업과 스타일링을 한 번에 끝낼 수 있었습니다. 더 이상 별도의 .module.css 파일을 만들고 import하는 번거로운 과정이 사라져, UI 레이아웃을 구성하는 속도가 이전 대비 2배 이상 단축되었습니다.

② 일관된 디자인 시스템(Design Token) 자동 유지

p-4 (16px), text-lg (18px), rounded-lg (8px) 등 Tailwind가 기본적으로 제공하는 규격화된 수치 단위만 사용하게 됩니다. 이 덕분에 마진이나 폰트 크기를 눈대중으로 어정쩡하게 설정하는 실수가 줄어들었고, 전체 페이지의 디자인 통일성이 자연스럽게 유지되었습니다.

tailwind로 작성한 코드

3. 실제로 부딪힌 단점: "HTML이 너무 더러워져요"

장점이 명확했던 만큼, 실무에서 부딪힌 치명적인 단점도 존재했습니다. 바로 JSX 태그의 className이 한도 끝도 없이 길어져 코드의 가독성을 심각하게 해친다는 점이었습니다.

TypeScript
 
// ❌ 가독성이 저하된 초기의 Tailwind 코드 예시
<button className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
  확인 버튼
</button>

버튼 하나를 만드는데 스타일 클래스가 수십 개나 나열되다 보니, 오랜만에 코드를 열었을 때 DOM 구조가 한눈에 들어오지 않고 스타일 코드에 가려지는 현상이 발생했습니다.

4. 가독성 단점을 해결한 나만의 리팩토링 노하우

이 문제를 극복하기 위해 프로젝트를 진행하며 다음과 같은 규칙을 세워 개선했습니다.

① @apply 지시어를 활용한 스타일 추상화

자주 반복 사용되는 버튼이나 인풋(Input) 스타일은 글로벌 CSS 파일 내에서 @apply 지시어를 사용하여 깔끔하게 공통 클래스로 묶어주었습니다.

CSS
 
/* main.css */
.btn-primary {
  @apply inline-flex items-center justify-center px-6 py-3 text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition-colors;
}

② React 컴포넌트 단위로 잘게 쪼개기

스타일이 복잡해진 영역은 그 자체로 재사용 가능한 작은 단위 컴포넌트로 분리하였습니다. 가령 공통 카드 UI, 뱃지, 모달 헤더 등을 별도 컴포넌트로 잘게 분리하니 JSX 본문의 가독성도 살리고 Tailwind의 이점도 그대로 유지할 수 있었습니다.

Tailwind로 작업된 결과물

5. 결론 및 총평

Tailwind CSS는 처음 접했을 때 "CSS를 HTML 안에 다시 집어넣는 퇴보가 아닌가?" 하는 지적을 받기도 합니다. 하지만 직접 경험해 본 Tailwind CSS는 단순한 인라인 스타일이 아니라, 잘 짜인 디자인 시스템을 가장 빠르게 마크업에 녹여낼 수 있는 강력한 생산성 도구였습니다.

클래스 이름 짓는 스트레스에서 벗어나 컴포넌트의 비즈니스 로직에 더 집중하고 싶은 프론트엔드 개발자분들은 Tailwind CSS 도입을  고려해보셨으면 좋을 것 같네요

웹사이트에 접속했을 때, 처음에는 기본 폰트로 텍스트가 보이다가 갑자기 예쁜 웹 폰트로 바뀌면서 줄 바꿈이 변하거나 문단이 아래로 툭 떨어지는 현상을 겪어보신 적이 있나요?

사용자를 당황하게 만드는 이 현상을 FOUT(Flash of Unstyled Text)라고 하며, 이는 구글의 핵심 성능 지표 중 하나인 CLS(Cumulative Layout Shift) 점수를 크게 갉아먹는 주범입니다. 오늘은 웹 폰트 로딩 최적화를 통해 시각적 안정성을 확보하는 실무 전략을 깊이 있게 알아보겠습니다.


1. CLS의 적, 웹 폰트 로딩의 이해

웹 폰트는 용량이 크기 때문에 브라우저가 HTML과 CSS를 파싱하는 속도보다 늦게 다운로드되는 경우가 많습니다. 이때 브라우저는 두 가지 방식 중 하나를 선택합니다.

  1. FOIT (Flash of Invisible Text): 폰트가 다운로드될 때까지 텍스트를 아예 보여주지 않음. (사용자는 빈 화면을 보게 됨)
  2. FOUT (Flash of Unstyled Text): 시스템 기본 폰트로 먼저 보여준 뒤, 웹 폰트가 로드되면 교체함. (이 과정에서 레이아웃이 어긋남)

구글은 사용자 경험을 위해 텍스트가 아예 안 보이는 것보다 레이아웃이 조금 흔들리더라도 빨리 보이는 것을 권장하지만, 이 '흔들림'을 최소화하는 것이 프론트엔드 개발자의 실력입니다.


2. font-display 속성으로 로딩 제어하기

가장 쉽고 필수적인 해결책은 CSS의 @font-face에서 font-display 속성을 사용하는 것입니다.

CSS
 
@font-face {
  font-family: 'Pretendard';
  src: url('/fonts/Pretendard.woff2') format('woff2');
  font-display: swap; /* 핵심 설정 */
}

2.1 주요 옵션 차이점

  • block: 폰트가 로드될 때까지 텍스트를 숨깁니다(최대 3초).
  • swap (권장): 시스템 폰트로 즉시 텍스트를 보여주고, 웹 폰트 로드가 완료되면 교체합니다. LCP 점수 유지에 유리합니다.
  • fallback: 아주 짧은 시간(약 0.1초) 동안 텍스트를 숨긴 후 로드되지 않으면 기본 폰트를 보여줍니다.
  • optional: 네트워크 상태에 따라 웹 폰트를 쓸지 말지 브라우저가 결정합니다. 성능이 최우선일 때 사용합니다.

3. 가상 폰트(Fallback Font) 최적화: 사이즈 매칭

font-display: swap을 사용하더라도 기본 폰트와 웹 폰트의 글자 크기, 자간, 높이가 다르면 여전히 레이아웃 시프트가 발생합니다. 이를 해결하기 위해 최근에는 가상 폰트(Size Adjust) 기법을 사용합니다.

3.1 size-adjust와 ascent-override

웹 폰트와 최대한 비슷한 부피를 가지도록 시스템 폰트의 수치를 강제로 조정하는 방식입니다.

CSS
 
/* 기본 폰트(Arial)를 웹 폰트와 크기가 비슷하게 조정 */
@font-face {
  font-family: 'Pretendard-Fallback';
  src: local('Arial');
  size-adjust: 95%;        /* 전체 크기 조정 */
  ascent-override: 90%;    /* 글자 윗부분 높이 조정 */
  descent-override: 10%;   /* 글자 아랫부분 높이 조정 */
}

body {
  font-family: 'Pretendard', 'Pretendard-Fallback', sans-serif;
}

이렇게 하면 폰트가 교체될 때 텍스트의 부피 변화가 거의 없어 CLS 점수를 0에 가깝게 유지할 수 있습니다. 최근에는 Capsize 같은 도구를 통해 이 수치를 정교하게 계산할 수 있습니다.


4. Next.js를 사용한다면? next/font 활용

Next.js (v13+) 환경에서 개발 중이라면 위 복잡한 과정이 대폭 자동화됩니다. next/font는 빌드 시점에 폰트를 최적화하고 가상 폰트를 자동으로 생성해 줍니다.

JavaScript
 
// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true, // 가상 폰트 자동 생성 옵션
});

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

next/font를 사용하면 폰트 파일을 구글 서버가 아닌 우리 서버(Self-hosting)에서 직접 제공하므로 개인정보 보호와 성능 면에서 모두 유리합니다.


5. 실무 체크리스트: 웹 폰트 100% 활용법

  1. 포맷 확인: 가장 압축률이 좋은 woff2 포맷을 우선적으로 사용하세요.
  2. Subset 활용: 한글 폰트는 용량이 매우 큽니다. 자주 쓰는 2,350자만 추린 '서브셋(Subset)' 폰트를 사용해 용량을 80% 이상 줄이세요.
  3. Preload: 중요한 폰트는 <link rel="preload">를 통해 미리 로드하세요.
  4. HTML
     
    <link rel="preload" href="/fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
    
  5. 로컬 폰트 활용: 사용자의 기기에 이미 설치된 폰트가 있다면 그것을 우선 사용하게 설정하세요. (src: local('Pretendard'), url(...))

+ Recent posts