Typewriter 애니메이션 구현하기

김현조·2023년 4월 5일
51

FrontEnd

목록 보기
8/9
post-thumbnail

ChatGPT의 등장에 그 놀라운 질의응답 기능 이외에도 관심을 끈 것이 있다면, 인터렉티브하게 답변을 눈앞에서 만들어낸다는 사실을 직관적으로 보여주는 typewriter 애니메이션이다.

“Typewriter”는 타자기를 의미한다. 타자기가 타닥타닥 글자를 치는 것과 같은 애니메이션을 typewriter 애니메이션이라 부른다. 웹에서는 글자를 하나씩 늘리기, 커서 옮기기를 구현함으로써 이를 만들어낼 수 있다.

만약 typewriter 애니메이션을 넣고자 하는 텍스트가 한줄이라면 css의 width 속성을 가지고도 구현이 가능하다. keyframe을 이용해 글자의 사이즈에 맞춰서 width를 0%부터 100%로 늘리면 된다.

.typewriter h1 {
  overflow: hidden; 
  border-right: .15em solid orange;
  white-space: nowrap
  margin: 0 auto; 
  letter-spacing: .15em;
  animation: 
    typing 3.5s steps(40, end),
    cursor .75s step-end infinite;
}

@keyframes typing {
  from { width: 0 }
  to { width: 100% }
}

@keyframes cursor{
  from, to { border-color: transparent }
  50% { border-color: orange; }
}

하지만 글자가 여러줄이라면 width만으로는 구현할 수 없다. 해당 경우에는 텍스트를 글자단위로 split하여 배열에 넣고 index를 하나씩 늘려가며 화면에 보여주는 방법이 있다. 아래의 방법은 Vanilla JavaScript를 사용한다면 적절한 방법일 수 있겠지만, React App에서는 더 좋은 방법을 제공한다.


function typewriter(element, text) {
  const speed = 100;
  let curIdx = 0;

  function write() {
    if (!text || !ref.current) return;

    element.innerHTML = text.substring(0, curIdx) + '|';
    if (curIdx++ === text.length) {
      curIdx = 0;
    } else {
      setTimeout(() => {
        write();
      }, speed);
    }
  }

  write();
}

아래와 같이 react state를 활용하는 방법이다. State를 index단위로 업데이트함으로써 렌더링을 시켜주면 된다. setTimeout의 delay를 활용해 속도를 조절할 수 있다.

const [text, setText] = useState('');

  useEffect(() => {
    const answer = result?.standardAnswer;
    let curIdx = 0;

    function write() {
      if (!answer) return;

      setText(answer.slice(0, curIdx));
      if (curIdx++ === answer.length) {
        curIdx = 0;
        const cursor = document.querySelector('.markdown.standard-answer') as HTMLElement;
        cursor.classList.add('done');
      } else {
        setTimeout(() => {
          write();
        }, 50);
      }
    }
    write();
  }, []);

커서는 CSS의 가상 요소로 주입할 수 있다. 작성 중에는 보여주고 완료 후 클래스 (”done”)을 추가하여 해당 클래스가 있는 경우 display: none으로 변경하는 것으로 구현 가능하다.

.markdown.standard-answer p:last-child::after {
  -webkit-animation: blink 1s infinite;
  animation: blink 1s infinite;
  content: '▋';
  margin-left: 0.25rem;
  vertical-align: baseline;
}

.markdown.standard-answer.done p:last-child::after {
  display: none;
}

@keyframes blink {
  from,
  to {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
}

6개의 댓글

comment-user-thumbnail
2023년 4월 8일

멋져요~

1개의 답글
comment-user-thumbnail
2023년 4월 9일

👍👍👍👍👍

1개의 답글
comment-user-thumbnail
2023년 4월 15일

우왕 신기해요

1개의 답글