타이핑 효과는 마치 누군가가 생각을 소리 내어 말하는 과정을 지켜보는 것과 같은 디지털적 경험을 제공합니다. 텍스트가 한 글자씩 나타나며, 마치 실제 손이 실제 키를 누르는 듯한 느낌을 줍니다. 포트폴리오 사이트의 히어로 섹션, 브라우저 기반 터미널 에뮬레이터, 그리고 미리 만들어진 텍스트 블록을 가져오는 것이 아니라 답변을 "타이핑"하고 있음을 증명하려는 AI 어시스턴트의 채팅창에서 이를 볼 수 있습니다. 잘 구현된 타이핑 효과는 기대감을 조성하지만, 잘못 구현되면 1987년에 고장 난 프린터처럼 느껴집니다.
이 패턴이 지속되는 이유
컴퓨터는 정보를 즉각적으로 전달하지만, 인간은 그렇지 않습니다. 이 두 속도 사이의 간극은 유용합니다. 타이핑 효과는 인간의 속도를 모방함으로써 그 간극을 메워줍니다. 랜딩 페이지에서는 헤드라인을 한 단어씩 시선을 끌며 보여줌으로써, 방문자가 내용을 대충 훑어보는 대신 가치 제안(value proposition)을 실제로 읽게 만들 수 있습니다. 터미널 에뮬레이터에서는 명령어가 실시간으로 실행되고 있다는 환상을 심어줍니다. 챗봇 인터페이스에서는 답변이 데이터베이스에서 단순히 검색된 것이 아니라 실시간으로 생성되고 있다는 신호를 보냅니다.
하지만 이 효과는 메커니즘이 사용자를 존중할 때만 작동합니다. 일정한 간격으로 똑같이 "틱-틱-틱" 소리가 나는 듯한 느낌은 로봇처럼 느껴집니다. 더 나아가, 보조 기술(assistive technology)을 고려하지 않은 구현은 즐거운 시각적 장치를 짜증 나는 장벽으로 바꿀 수 있습니다. 목표는 사용자의 속도를 늦추는 것이 아니라, 인터페이스가 살아있는 것처럼 느껴지도록 적절한 마찰(friction)을 더하는 것입니다.
재귀적 setTimeout으로 구현하기
setTimeout으로 시작하세요. setInterval은 건드리지 마세요. 그 차이는 생각보다 큽니다.
setInterval은 경직되어 있습니다. 스크립트나 브라우저의 메인 스레드에서 다른 일이 일어나든 말든 매 n 밀리초마다 실행됩니다. 만약 일반적인 키 입력 사이에는 50밀리초의 일시 정지가 필요하고, 문장 부호 뒤에는 150밀리초의 일시 정지가 필요하다면, setInterval은 이에 적응할 수 없습니다. 결국 추가적인 조건부 로직을 덧붙이고, 레이스 컨디션(race conditions)과 싸우며, 인터벌을 너무 자주 해제하고 재설정하게 되어 코드가 상태 관리의 악몽이 되고 맙니다. 고정된 인터벌은 가변적인 속도가 필요한 순간 바로 한계에 부딪힙니다.
재귀적 setTimeout은 각 단계가 다음 단계의 규칙을 스스로 결정하게 함으로써 이 문제를 해결합니다. 이를 작은 상태 머신(state machine)이라고 생각하세요. 현재 문자열, 현재 문자 인덱스, 타이핑 중인지 삭제 중인지를 나타내는 불리언 플래그, 그리고 여러 문자열을 순회할 수 있는 textIndex 카운터와 같은 몇 가지 변수를 유지합니다. 함수는 한 글자를 추가하고, 문장에서 현재 위치가 어디인지 확인한 다음, 문맥에 맞는 지연 시간을 설정하여 다음 호출을 예약합니다.
예를 들어, 대부분의 문자는 50밀리초 간격으로 입력하고, 쉼표 뒤에는 150밀리초로 늦추며, 문장이 끝날 때는 삭제 모드로 전환하기 전 800밀리초 동안 멈출 수 있습니다. setInterval로는 이를 깔끔하게 처리할 수 없습니다. 재귀적 setTimeout을 사용하면 로직은 명확합니다:
if typing:
append next character
if at end of string:
switch to pause mode
schedule next call after 1000ms
if deleting:
remove last character
if string empty:
increment textIndex
load next string
switch to typing mode
이 구조는 정리(cleanup) 작업도 매우 간단하게 만듭니다. 타임아웃 ID를 저장해 두세요. 컴포넌트가 언마운트되거나 사용자가 페이지를 벗어날 때 clearTimeout을 한 번만 호출하면 됩니다. 백그라운드에서 틱틱거리는 고아 인터벌(orphaned intervals)이 남지 않습니다.
커서는 스스로 깜빡여야 합니다
깜빡이는 커서는 시각적인 디테일이지, 데이터와 관련된 문제가 아닙니다. 자바스크립트 상태 엔진에서 커서를 분리하세요. ::after 가상 요소에 연결된 별도의 CSS 애니메이션이나, 텍스트 컨테이너 끝에 위치한 전용 <span>을 사용하세요.
step-end 타이밍을 사용하여 opacity나 border-color를 전환하는 간단한 @keyframes blink는 컴포지터(compositor)에서 실행되는 깔끔하고 하드웨어 친화적인 펄스 효과를 제공합니다. 자바스크립트가 커서의 가시성을 일일이 관리할 필요는 없습니다. 만약 setTimeout 재귀 내부에서 디스플레이 속성을 토글한다면, 매 글자마다 불필요한 스타일 재계산(style recalculations)을 강제하게 됩니다. 미학적인 부분은 CSS에 맡기고, 시퀀스는 자바스크립트에 맡기세요.
textIndex 카운터를 사용하면 여러 문자열을 순회하는 것도 간단합니다. 문자열들을 배열에 저장하세요. 애니메이션의 삭제 단계가 끝나고 컨테이너가 비워지면, textIndex를 배열 길이의 나머지 값으로 증가시키고, 문자 포인터를 0으로 재설정한 뒤 다시 타이핑을 시작합니다. 포트폴리오 사이트가 페이지를 새로고침하지 않고도 ["Developer", "Designer", "Writer"]와 같은 직무를 순환하며 보여주는 방식이 바로 이것입니다.
환상을 깨뜨리는 실수들
아마추어의 구현에서 반복적으로 나타나는 세 가지 오류가 있습니다.
setInterval 사용하기. 가변 속도 문제는 이미 다루었지만, 더 미묘한 문제가 있습니다. 브라우저가 레이아웃 시프트를 그리느라 DOM 업데이트가 지연되는 경우, setInterval은 계속 실행됩니다. 이로 인해 쓰기 작업이 겹치거나, 문자가 중복되거나, 브라우저가 렌더링할 수 있는 속도보다 더 빠르게 쓰기가 발생할 수 있습니다. 재귀적 setTimeout은 다음 단계를 생각하기 전에 현재 단계가 완료될 때까지 기다립니다.
HTML 이스케이프 누락. 소스 문자열에 꺽쇠괄호가 포함되어 있고 innerHTML을 통해 한 글자씩 콘텐츠를 주입하면, 태그가 중간에 잘릴 수 있습니다. 브라우저는 <, 그 다음 <s, 그 다음 <st를 보게 됩니다. 이는 적절한 태그 파싱을 방해하며, 깨진 DOM 노드나 예상치 못한 스타일링 캐스케이드를 남길 수 있습니다. 문자를 그대로 표시하고 싶다면 먼저 이스케이프 처리를 하거나, 더 좋은 방법은 innerHTML 대신 textContent에 쓰는 것입니다. 타이핑 출력물 내부에 스타일이 적용된 span이 꼭 필요하다면, 문자 루프를 시작하기 전에 태그의 시작과 끝을 정확히 알 수 있도록 문자열을 전처리하세요.
접근성 무시. 스크린 리더는 한 글자씩 읽어주는 것을 좋아하지 않습니다. 스크립트가 DOM에 새 문자를 추가할 때마다 일부 보조 기술은 전체 노드를 다시 발표하여, 단어가 끊어지는 듯한 연타가 발생합니다. 이는 청각적 탐색에 의존하는 사용자에게 악몽과 같습니다. 해결 방법은 복잡하지 않습니다. 전체 최종 텍스트를 담고 있는 컨테이너에 aria-label을 추가하세요. 또한 aria-hidden="true"를 사용하여 애니메이션 요소를 보조 기술로부터 완전히 숨기고, 스크린 리더용으로 시각적으로 숨겨진 정적 복사본을 제공할 수도 있습니다. 어떤 방식이든, 사용자가 당신의 퍼포먼스를 견디게 하는 대신 전체 문장을 미리 제공하세요.
버전을 다듬는 방법
핵심 루프가 제대로 작동하면 추가적인 기능을 얹을 수 있습니다. 하지만 기본기가 탄탄해질 때까지는 추가하고 싶은 유혹을 참으세요.
키 입력 효과음. 각 문자마다 미세한 클릭 소리를 넣으면 만족감을 줄 수 있지만, 웹페이지의 오디오는 지뢰밭과 같습니다. Web Audio API를 사용하거나 짧은 버퍼를 가진 가벼운 Audio 요소를 사용하세요. 동일한 클릭 소리가 합성된 것처럼 들리지 않도록 재생 속도를 0.95에서 1.05 사이로 약간씩 변화를 주십시오. 항상 브라우저의 자동 재생 정책을 준수하고 음소거 토글을 제공해야 합니다. 오전 9시에 타이핑 소리가 자동 재생되는 포트폴리오 페이지만큼 사용자를 빨리 쫓아버리는 것도 없습니다.
멀티라인 타이핑. 실제 터미널 창은 줄 바꿈이 일어납니다. 레이아웃이 예측 가능하지 않다면, 텍스트가 줄 바꿈을 넘을 때 단순한 border-right 커서는 어색하게 튈 것입니다. 줄 바꿈 문자를 기준으로 문자열을 나누고 각 줄을 별도의 <span>에 렌더링하거나, 콘텐츠의 끝을 추적하는 위치 지정된 의사 요소(pseudo-element)를 사용하세요. 텍스트 줄 바꿈에 주의하십시오. 인라인 테두리로 구현된 커서는 컨테이너 너비가 변하면 텍스트에서 떨어질 수 있습니다. 터미널 스타일의 경우 white-space: pre-wrap과 고정폭 글꼴(monospaced font)을 사용하는 것을 고려하세요. 고정 너비 문자는 커서 계산을 훨씬 더 예측 가능하게 만듭니다.
실시간 Markdown 렌더링. 이 부분이 까다로운 지점입니다. **bold**를 입력할 때 두 가지 선택지가 있습니다. 별표를 나타나는 그대로 문자로 렌더링하거나, 즉석에서 굵은 스타일로 변환하는 것입니다. 후자를 선택하면 스트림 중간에 textContent에서 innerHTML로 전환하는 순간 텍스트 노드의 경계가 변경됩니다. HTML 태그가 아래의 DOM 트리를 이동시키기 때문에 커서 위치를 관리하는 것이 골칫거리가 됩니다. 더 안전한 접근 방식 중 하나는 원본 Markdown 문자열을 정상적으로 입력한 다음, 전체 문자열이 화면에 나타나면 렌더링 패스를 트리거하는 것입니다. 실시간 포맷팅이 정말로 필요하다면, 숨겨진 타이핑 버퍼와 파싱된 시각적 오버레이라는 두 개의 레이어를 유지하세요.
삭제 효과. 텍스트를 삭제한다고 해서 반드시 한 글자씩 백스페이스를 누를 필요는 없습니다. 다음 문자열이 타이핑되기 전에 필드를 즉시 비우는 "전체 선택 후 삭제" 리셋을 시뮬레이션할 수 있습니다. 이는 다소 기계적으로 느껴질 수 있습니다. 반대로, 글자당 30밀리초의 느린 백스페이스는 긴장감을 조성합니다. 이 두 가지를 섞어보세요. 오타를 빠르게 백스페이스로 지우고, 잠시 멈췄다가, 다시 정상 속도로 삭제를 재개하는 식입니다. 이러한 변화가 효과에 인간적인 느낌을 더해줍니다.
핵심 요약
타이핑 효과는 겉보기에는 사소해 보이지만, 직접 만들어보고 나서야 그 복잡함이 드러나는 UI 장식 중 하나입니다. 시작은
