📖react-bits 파헤치기·1/6단계
17%

CSS만으로 글리치 텍스트 만들기

react-bits의 텍스트 애니메이션들이 어떻게 구현되었는지, 어떤 기술로 구현했는지를 살펴본다.

글리치 이펙트가 뭐냐?

간단히 말해서 지글거리는 텍스트라고 할 수 있을 것 같다. 오류, 노이즈 같은 시각적 표현이다.

주요 표현 기법은 아래와 같다고 한다.

  • RGB 분리(Chromatic Aberration) — 빨강/초록/파랑 채널을 살짝 어긋나게 배치
  • 스캔라인 — 가로줄이 보이는 CRT 모니터 느낌
  • 픽셀 소팅(Pixel Sorting) — 픽셀을 밝기나 색상 기준으로 정렬해서 흘러내리는 효과
  • 데이터모싱(Datamoshing) — 영상 압축 프레임을 의도적으로 깨뜨리는 기법
  • 슬라이스/디스플레이스먼트 — 이미지 일부를 수평으로 잘라서 어긋나게 배치
  • 노이즈/지터 — 랜덤한 떨림이나 깜빡임 추가

react-bits의 컴포넌트를 확인해보자

GLITCH
0.50s

위에서 나열한 글리치 기법 중 이 컴포넌트는 RGB 분리(text-shadow로 빨강/청록을 좌우로 어긋나게 배치)와 슬라이스(clip-path: inset()으로 수평 가로띠를 잘라서 어긋나게 배치), 이 두 가지를 사용한다.

코드 해부: 전체 코드 먼저 보기

먼저 전체 코드를 훑어보자.

GlitchText.tsx
import { FC, CSSProperties } from 'react';
import './GlitchText.css';

interface GlitchTextProps {
  children: string;
  speed?: number;
  enableShadows?: boolean;
  enableOnHover?: boolean;
  className?: string;
}

interface CustomCSSProperties extends CSSProperties {
  '--after-duration': string;
  '--before-duration': string;
  '--after-shadow': string;
  '--before-shadow': string;
}

const GlitchText: FC<GlitchTextProps> = ({
  children,
  speed = 0.5,
  enableShadows = true,
  enableOnHover = false,
  className = ''
}) => {
  const inlineStyles: CustomCSSProperties = {
    '--after-duration': `${speed * 3}s`,
    '--before-duration': `${speed * 2}s`,
    '--after-shadow': enableShadows ? '-5px 0 red' : 'none',
    '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'
  };

  const hoverClass = enableOnHover ? 'enable-on-hover' : '';

  return (
    <div
      className={`glitch ${hoverClass} ${className}`}
      style={inlineStyles}
      data-text={children}
    >
      {children}
    </div>
  );
};

export default GlitchText;

React가 하는 일은 딱 하나다. CSS에 값을 전달하는 브릿지 역할뿐이다. 실제 애니메이션 로직은 전부 CSS에 있다.

여기서 주목할 점 세 가지:

  1. style 속성으로 CSS Custom Properties를 주입한다
  2. data-text={children}으로 텍스트를 HTML 속성에도 복사한다
  3. speed * 3과 speed * 2 — 두 레이어의 속도를 의도적으로 다르게 설정한다

왜 이렇게 하는지, 하나씩 뜯어보자.


핵심 기법 1: CSS Custom Properties

CSS 변수란

CSS Custom Properties(CSS 변수)는 --로 시작하는 사용자 정의 속성이다. var() 함수로 참조한다.

.element {
  --my-color: red;
  color: var(--my-color);        /* red */
  background: var(--bg, white);  /* fallback: white */
}

일반 CSS 속성과 다른 점:

  • 상속된다 — 부모에 선언하면 자식에서 사용 가능
  • 런타임에 변경 가능 — JavaScript로 style.setProperty('--my-color', 'blue') 하면 즉시 반영
  • var()에 fallback 지정 가능 — 변수가 없으면 두 번째 인자를 사용

이 코드에서의 사용

// React에서 CSS 변수를 주입
const inlineStyles = {
  '--after-duration': `${speed * 3}s`,   // 예: 1.5s
  '--before-duration': `${speed * 2}s`,  // 예: 1.0s
  '--after-shadow': enableShadows ? '-5px 0 red' : 'none',
  '--before-shadow': enableShadows ? '5px 0 cyan' : 'none'
};
/* CSS에서 var()로 소비 */
.glitch::after {
  text-shadow: var(--after-shadow, -10px 0 red);
  animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;
}

React props → CSS 변수 → var() 참조. 이 패턴의 장점은 CSS 애니메이션의 성능을 그대로 유지하면서 JavaScript로 제어할 수 있다는 것이다. 만약 props가 바뀔 때마다 JavaScript로 직접 스타일을 조작했다면, 매 프레임 계산에 JS가 개입해야 한다. CSS 변수 방식은 변수 값만 바꿔주면 나머지는 브라우저의 CSS 엔진이 처리한다.

var(--after-duration, 3s)에서 3s는 fallback 값이다. React가 inline style을 주입하지 않는 상황(예: SSR에서 hydration 전)에서도 애니메이션이 동작한다.

왜 duration을 다르게?

'--after-duration': `${speed * 3}s`,   // ::after → 3배
'--before-duration': `${speed * 2}s`,  // ::before → 2배

두 레이어가 같은 keyframe을 쓰는데, 주기가 같으면 완전히 동기화되어 한 몸처럼 움직인다. 그러면 "깨진 느낌"이 아니라 "같이 흔들리는 느낌"이 된다.

주기를 2배와 3배로 다르게 주면? speed = 0.5일 때 ::before는 1초, ::after는 1.5초 주기로 돌아간다. 같은 프레임이 동시에 나오는 순간은 3초에 한 번뿐이고, 나머지 시간에는 항상 서로 다른 위치를 보여준다. 덕분에 규칙적인 패턴 없이 "이리저리 제각각 깨지는" 느낌을 만들 수 있다.


핵심 기법 2: data-* 속성과 attr()

data-text가 왜 필요한가

<div data-text={children}>
  {children}  {/* "GLITCH" */}
</div>

HTML에 같은 텍스트를 두 번 넣는다. 왜? CSS pseudo-element(::before, ::after)는 DOM 노드가 아니라서 children에 직접 접근할 수 없기 때문이다.

attr() 함수

.glitch::after,
.glitch::before {
  content: attr(data-text);  /* "GLITCH" */
}

attr() 함수는 해당 요소의 HTML 속성값을 가져온다. 현재 CSS 스펙에서 attr()은 content 속성에서만 사용 가능하다. (CSS Values Level 5에서 다른 속성에도 쓸 수 있도록 제안 중이지만 아직 브라우저 지원이 없다.)

이 구조 덕분에 텍스트가 바뀌어도 CSS를 수정할 필요가 없다. React에서 data-text 속성만 바꾸면 pseudo-element의 content도 자동으로 업데이트된다.


핵심 기법 3: Pseudo-elements 레이어링

왜 3개 레이어인가

결과적으로 화면에는 같은 텍스트가 3겹으로 쌓인다:

레이어역할위치 오프셋색상 그림자
원본 (.glitch)기준 텍스트0없음
::before왼쪽으로 어긋난 복사본left: -10pxcyan (+5px)
::after오른쪽으로 어긋난 복사본left: 10pxred (-5px)

이게 바로 Chromatic Aberration(색수차)이다. 실제 CRT 모니터에서 R/G/B 전자빔이 미세하게 어긋나는 현상을 시뮬레이션한 것이다. 텍스트 자체를 좌우로 어긋나게 배치하고, text-shadow로 반대 방향에 색상 그림자를 추가해서 "빨강과 청록이 번지는" 느낌을 만든다.

CSS 코드 해부

.glitch {
  color: #fff;
  position: relative;  /* pseudo-element 배치의 기준점 */
}

.glitch::after,
.glitch::before {
  content: attr(data-text);
  position: absolute;    /* 원본 위에 겹침 */
  top: 0;                /* 원본과 같은 위치에서 시작 */
  color: #fff;
  background-color: #060010;
  overflow: hidden;
  clip-path: inset(0 0 0 0);  /* 초기 상태: 전체 보임 */
}

/* ::after — 오른쪽으로 어긋남 */
.glitch:not(.enable-on-hover)::after {
  left: 10px;
  text-shadow: var(--after-shadow, -10px 0 red);
  animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;
}

/* ::before — 왼쪽으로 어긋남 */
.glitch:not(.enable-on-hover)::before {
  left: -10px;
  text-shadow: var(--before-shadow, 10px 0 cyan);
  animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;
}

position: relative인 부모 위에 position: absolute인 pseudo-element 두 개를 올린다. top: 0으로 원본과 같은 위치에서 시작하되, left 값으로 좌우로 살짝 어긋나게 배치한다. :not(.enable-on-hover) 선택자는 enableOnHover가 false일 때(= 항상 애니메이션)만 적용되도록 한다.

text-shadow는 텍스트에 그림자를 추가한다. text-shadow: x방향 y방향 blur 색상 순서로, 이 코드에서는 text-shadow: -5px 0 red처럼 x방향만 사용한다. text-shadow의 방향이 left와 반대인 것에 주목하자. ::after는 left: 10px(오른쪽)인데 그림자는 -5px(왼쪽)이다. 이렇게 하면 텍스트 본체와 그림자가 양쪽으로 퍼져서 색수차 범위가 넓어진다.


핵심 기법 4: clip-path와 @keyframes

clip-path: inset()

clip-path는 요소의 보이는 영역을 정의한다. 사용할 수 있는 도형 함수는 여러 가지가 있다:

  • inset() — 사각형. 상/우/하/좌에서 잘라냄
  • circle() — 원. circle(50% at 50% 50%)
  • ellipse() — 타원. ellipse(40% 60% at 50% 50%)
  • polygon() — 다각형. 꼭짓점을 자유롭게 지정
  • path() — SVG path 문법 그대로 사용

이 코드에서는 글리치 효과가 가로띠(수평 슬라이스) 형태니까, 위/아래만 잘라내면 되는 inset()이 가장 적합하다.

clip-path: inset(20% 0 50% 0);
/*          위 20% 잘림  |  오른쪽 안 잘림  |  아래 50% 잘림  |  왼쪽 안 잘림 */
/* 결과: 전체 높이의 20%~50% 구간만 보임 (가로띠 형태) */

시각적으로 표현하면:

GLITCH
::after
GLITCH
::before
GLITCH원본 텍스트

드래그해서 여러 방향에서 확인해보자

이 값이 keyframe마다 바뀌면서 수평 슬라이스가 위아래로 움직이는 효과가 만들어진다.

keyframes 해부

@keyframes animate-glitch {
  0%   { clip-path: inset(20% 0 50% 0); }  /* 20~50%: 30% 높이의 띠 */
  5%   { clip-path: inset(10% 0 60% 0); }  /* 10~40%: 30% 높이의 띠 */
  10%  { clip-path: inset(15% 0 55% 0); }  /* 15~45%: 30% 높이의 띠 */
  15%  { clip-path: inset(25% 0 35% 0); }  /* 25~65%: 40% 높이의 띠 */
  20%  { clip-path: inset(30% 0 40% 0); }  /* 30~60%: 30% 높이의 띠 */
  25%  { clip-path: inset(40% 0 20% 0); }  /* 40~80%: 40% 높이의 띠 */
  /* ... 21개 프레임까지 계속 */
}

패턴을 분석해보면:

  • 좌우(2번째, 4번째 값)는 항상 0 — 수평 방향으로는 자르지 않는다. 가로 전체를 보여준다.
  • 위/아래 값만 변화 — 보이는 가로띠의 위치가 위아래로 점프한다.
  • 5% 간격 — 21단계로 촘촘하게 변화하지만, 값 자체는 불규칙하다.
  • 보이는 영역의 높이가 30%~40% 사이에서 변한다 — 띠의 두께도 미세하게 다르다.

animation-direction

animation: animate-glitch 3s infinite linear alternate-reverse;

animation-direction에는 네 가지 값이 있다:

값재생 순서
normal0% → 100%, 0% → 100%, ...
reverse100% → 0%, 100% → 0%, ...
alternate0% → 100%, 100% → 0%, ...
alternate-reverse100% → 0%, 0% → 100%, ...

여기서 중요한 건 alternate-reverse냐 alternate냐가 아니라, 왕복 재생 자체다. normal이면 0% → 100%까지 재생한 뒤 다시 0%로 점프하기 때문에 끊기는 느낌이 난다. alternate 계열은 100%에 도달하면 역순으로 되돌아오기 때문에 끊김 없이 자연스럽게 이어진다. alternate와 alternate-reverse의 차이는 첫 사이클의 방향뿐이라 결과는 거의 같다.

정리하면 불규칙한 글리치를 만드는 핵심은 두 가지다:

  1. 다른 duration (2배 vs 3배) — 두 레이어의 주기가 다르면 같은 프레임이 겹치는 순간이 드물어진다
  2. 21단계의 불규칙한 clip-path 값 — keyframe 자체가 불규칙한 패턴

Pure CSS의 한계

이 구현은 꽤 그럴듯한 글리치를 만들지만, CSS만으로는 넘을 수 없는 벽이 있다.

진짜 랜덤이 불가능하다

  • keyframe 값은 빌드 타임에 고정된다. 아무리 촘촘하게 프레임을 만들어도 결국 반복 패턴이다.
  • JavaScript의 Math.random()이나 Perlin Noise 같은 실시간 랜덤은 CSS만으로 불가능하다.

마우스 위치에 반응할 수 없다

  • :hover는 "위에 있다/없다" 바이너리 상태만 감지한다.
  • "마우스가 가까워질수록 글리치가 심해진다" 같은 연속적 반응은 JavaScript가 필요하다.

물리 기반 움직임이 없다

  • CSS transition과 animation은 시간 기반(duration, easing)이다.
  • 스프링 물리(질량, 탄성, 감쇠)처럼 "관성에 의해 흔들리는" 움직임은 구현이 매우 어렵다.

다른 요소와의 상호작용이 제한적이다

  • CSS는 기본적으로 자기 자신의 상태만 알 수 있다.
  • "옆 요소가 글리치되면 나도 영향받는다" 같은 연쇄 반응은 CSS 선택자만으로는 한계가 있다.

그래서 react-bits의 23개 텍스트 애니메이션 중 순수 CSS만으로 구현된 건 이 GlitchText가 거의 유일하다. 나머지는 Framer Motion이나 GSAP 같은 JavaScript 라이브러리를 사용한다.

다음 포스트에서는

JavaScript가 필요한 순간 — Framer Motion으로 선언적 애니메이션을 다룬다. react-bits의 23개 텍스트 애니메이션 중 11개가 Framer Motion을 사용하는 이유를 파헤친다.

  • 글리치 이펙트가 뭐냐?
  • react-bits의 컴포넌트를 확인해보자
  • 코드 해부: 전체 코드 먼저 보기
  • 핵심 기법 1: CSS Custom Properties
  • CSS 변수란
  • 이 코드에서의 사용
  • 왜 duration을 다르게?
  • 핵심 기법 2: data-* 속성과 attr()
  • data-text가 왜 필요한가
  • attr() 함수
  • 핵심 기법 3: Pseudo-elements 레이어링
  • 왜 3개 레이어인가
  • CSS 코드 해부
  • 핵심 기법 4: clip-path와 @keyframes
  • clip-path: inset()
  • keyframes 해부
  • animation-direction
  • Pure CSS의 한계
  • 다음 포스트에서는
  • 글리치 이펙트가 뭐냐?
  • react-bits의 컴포넌트를 확인해보자
  • 코드 해부: 전체 코드 먼저 보기
  • 핵심 기법 1: CSS Custom Properties
  • CSS 변수란
  • 이 코드에서의 사용
  • 왜 duration을 다르게?
  • 핵심 기법 2: data-* 속성과 attr()
  • data-text가 왜 필요한가
  • attr() 함수
  • 핵심 기법 3: Pseudo-elements 레이어링
  • 왜 3개 레이어인가
  • CSS 코드 해부
  • 핵심 기법 4: clip-path와 @keyframes
  • clip-path: inset()
  • keyframes 해부
  • animation-direction
  • Pure CSS의 한계
  • 다음 포스트에서는
📚

react-bits 파헤치기

  1. 🔍1. CSS만으로 글리치 텍스트 만들기
  2. ⏳2. Motion: React에서 선언적으로 애니메이션하기
  3. ⏳3. GSAP + ScrollTrigger: 타임라인으로 정밀하게 제어하기
  4. ⏳4. Canvas 2D: 텍스트를 픽셀로 다루기
  5. ⏳5. WebGL & Shader: GPU로 텍스트 렌더링하기
  6. ⏳6. Variable Font & SVG: 브라우저 내장 기술의 가능성
Motion: React에서 선언적으로 애니메이션하기→

관련 포스트

이 포스트와 관련된 다른 글들을 확인해보세요

CSS만으로 글리치 텍스트 만들기

JavaScript 없이 CSS keyframes, pseudo-elements, custom properties만으로 글리치 효과를 구현한다. CSS 애니메이션의 가능성과 한계를 탐구한다.

2026년 2월 16일•5분
cssanimationkeyframes+2

GSAP + ScrollTrigger: 타임라인으로 정밀하게 제어하기

Motion과 GSAP는 어떻게 다른가? timeline, scrub, stagger, SplitText 플러그인으로 스크롤 기반 텍스트 애니메이션을 구현한다.

2026년 2월 20일•12분
gsapscroll-triggeranimation+2

프론트엔드 UI 스타일 카탈로그 — 67개 스타일 실전 가이드

Glassmorphism부터 Neubrutalism까지, 67개 UI 스타일을 카테고리별로 정리하고 CSS 구현 핵심과 장단점을 비교했다.

2026년 4월 8일•9분
ui-uxcssdesign+1

Comments