개발자일기/Tutorial

타이핑 애니메이션으로 텍스트를 좀 더 그럴 듯 하게 만들어보자!! (VanillaJS)

뫙뭉 2025. 3. 24. 23:07
반응형

각자의 홈페이지를 꾸미고 마케팅하기 위해 눈에 띄는 애니메이션들을 만들고 적용하곤 한다.

예전에 몇가지 글을 적은적이 있는데, 

별 생각 없이 올렸던 글이 생각보다 많은 분들이 관심을 가져주더라.

기술적인 부분도 좋지만, 이런 간단하면서 뭔가 유용한 애니메이션, 스타일 관련 글에도 많은 분들이 관심을 갖고 있구나 라는 걸 다시 한번 느꼈다.

 

2022.02.03 - [개발자일기/Tutorial] - 빛번짐 효과, box-shadow & filter 활용하기 (feat. ionicons)

 

빛번짐 효과, box-shadow & filter 활용하기 (feat. ionicons)

단 몇 줄의 코드로 색다른 효과를 낼 수 있다! 오늘도 코드가 원하는대로 짜지지 않아 김빠짐을 경험하여 유튜브와 코드팬속에서 헤엄치던 중...역시나 사이트에 재미를 선사하는 요소로 hover ef

mwangmoong.tistory.com

2022.01.13 - [개발자일기/Tutorial] - 한 글자씩 나타나는 애니메이션(CSS, VanilaJS)

 

한 글자씩 나타나는 애니메이션(CSS, VanilaJS)

웹사이트에 심심함을 덜어줄 무언가가 필요하다!! 얼마전에 포트폴리오를 재정비할 계획이 있었다.사실 신입 포트폴리오, 심지어 비전공자라 하면 쓸 말이 많지가 않더라. 나만 그런것인가...

mwangmoong.tistory.com

 

 

그래서 오늘은 오랜만에!!

많은 분들이 포트폴리오, 랜딩 페이지 등에 사용하고 있는

타이핑 애니메이션(Typing animation) 에 대해 적어볼까 한다.

이 글에서는 VanillaJS를 활용하여 웹사이트에 타이핑 애니메이션을 구현하는 방법을 단계별로 설명한다.

 

 

별거 아닌 것 같지만, 이런게 있어주면 또 이뻐보이는 효과가 있다!!

 

 

목표부터 확실히 정하자!

이번 글의 목표는 다음과 같다.

- Javascript를 통해 동적으로 element를 생성한다.

- 여러 개의 문자열을 배열로 관리한다.

- 반짝이는 커서까지 만든다.

- 입력되고 지워지고 지연시간까지 변수로 관리한다.

- VanillaJS와 CSS만을 이용하여 애니메이션 개발을 완료한다!

 

바로 본론으로 들어가보자

 

 

우선 html과 css 부터 만들자

우리는 javascript를 통해 동적으로 요소를 생성할 것이기 때문에, html은 복잡할 것이 없다.

이 타이핑되는 요소가 어디에 들어갈지만 정의하면 된다.

여러 요소가 있으면 어느 위치에 들어갈지 잘 생각하여 type-container div 를 넣도록 하자.

 

그리고 style.css, index.js도 넣는 것을 잊지 말자.

/* index.html */

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="typed-container">
    </div>
    <script src="index.js"></script>
</body>
</html>

 

 

요소가 적다보니 style.css는 어려울 것 없다.

다만 typed-container의 크기나 위치가 상황에 따라 다를테니 그것만 잘 유의해서 style 코드를 짜보자.

나는 어두운 배경이 좋아서 body의 background-color도 넣어줬다.

 

그리고 마지막으로, 실제 타이핑 요소가 될 typed-content의 스타일도 간단히 적어주자.

이 부분은 사실 javascript로 요소를 생성할 때, style 속성에 넣어도 되지만,

필자 생각에는 이런 코드는 style 코드들이 모여있는 css 파일에서 관리하는 것이 더 좋다고 생각한다.

각자의 논리에 맞게 코드를 짜면 된다. 논리만 맞다면, 그리고 그게 효율적이라면, 정답은 없다.

 

/*  style.css */

body {
    background-color: #373544;
    color: #fff;
}
.typed-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.typed-content {
    font-size: 4rem;
    font-weight: 700;
}

.cursor{
    width: 2px;
    height: var(--typed-content-size);
    background-color: white;
}

 

뼈대가 완성되었으니, 본격적으로 javascript를 통해 기능을 구현해보자.

 

Javascript에 어떤 것들을 넣으면 될까?

일단 이 타이핑 애니메이션에 필요한 몇가지 변수들이 있다.

이건 구현 요건에 따라 다르겠지만, 내가 선정한 변수들은 다음과 같다.

 

  • texts : array로 표시할 텍스트들을 나열한다.
  • typingSpeed : 타이핑 속도
  • deleteSpeed : 삭제 속도
  • delayAfterTyping: 타이핑 후 대기시간
  • deleteAfterDelete : 타이핑 지워진 후 대기시간
  • cursorSetting : 커서의 유무, 깜빡임 속도 속성을 설정

 

필자가 필요한 설정들을 넣었으나, 창의력이 뛰어나신 분들은 여기에 이것저것 추가를 하면 되겠다.

변수가 많다는 것은 그만큼 핸들링 할 수 있는 요소가 많다는 것이기 때문에, 추가하다 보면 더 재밌는 스타일이 생기거나 편의성이 개선되는 효과가 있을 수 있겠다.

 

그럼 이제 코드를 짜보자.

 

우선, 어떤 부모 요소에 타이핑 컨텐츠를 추가할지 설정해야 한다. 나는 이 또한 변수로 설정하였다.

 

// index.js

const TYPED_ANIMATION_CONFIG = {
  texts: ["안녕하세요", "뫙뭉 블로그입니다", "환영합니다"], // 표시할 텍스트 목록
  typingSpeed: 100,         // 타이핑 속도 (ms)
  deleteSpeed: 100,         // 삭제 속도 (ms)
  delayAfterTyping: 1000,   // 타이핑 후 대기 시간 (ms)
  delayAfterDelete: 500,    // 삭제 후 대기 시간 (ms)
  cursorSettings: {
    cursorOn: true,
    blinkSpeed: 500         // 커서 깜빡임 속도 (ms)
  }
};

const TYPED_ANIMATION_SELECTORS = {
  container: ".typed-container"  // 타이핑 애니메이션이 들어갈 컨테이너의 클래스명
};

 

 

개발을 조금 해보신 분이라면 알겠지만, 변수를 설정하는 이유는, 여러 곳에서 사용되는 데이터를 한 곳에서 관리하기 용이하다. 나중에 수정할 때, 여기에서 딸깍 바꾸기만 하면 된다는 말씀.

 

이제 메인 함수를 만들어보자.

 

이 메인함수의 가장 핵심적인 요소는, 바로 '타이핑' 이다.

그렇다면 타이핑을 어떻게 구현하면 좋을까?

시작하기 전에

한 기능을 구현하는 방법은 정말 다양하게 존재한다.

필자가 구현한 방법 말고, 본인이 더 나은 방법이 있다면, 그리고 그것이 오류 없이 원하는대로 잘 동작하면 그것도 정답이다. 필자의 방법이 다른 여러 효율적인 방법들을 생각해내는데에 도움이 되길 바란다.

 

자 그럼 필자의 아이디어는 이러하다.

 

요소에 글자를 하나씩 추가하면 타이핑 하는 것처럼 보인다는 것.

안녕하세요를 출력한다고 하면,
'안'
'안녕'
'안녕하'
'안녕하세'
'안녕하세요'
이런식으로 순차적으로 출력한다면, 타이핑 되는 것처럼 보일거고,
반대로 실행한다면 지워지는 것처럼 보일 것이다.

 

 

일단 아이디어는 해결 되었고,

이제 어떻게 구현하면 될지 생각하며 변수부터 선언해보자

 

필요한 변수를 선언해보자

함수 안에서 선언해야 할 변수는 뭐가 있을까?

일단 TYPED_ANIMATION_CONFIG.texts에 입력한 글자들을 출력해야하는데,

 

  • index : 어떤 글자까지 출력했는지를 ('안'인지, '안녕'인지) 표시하기 위한 변수,
  • contentCount : 몇번째 컨텐츠를 출력하였는지 ('안녕하세요' 인지, '뫙뭉 블로그입니다' 인지)를 표시하기 위한 변수
  • timeoutId : 동작시킬 setTimeout을 저장하는 변수

 

이렇게 세가지이다.

필자는 setTimeout을 통해서 typing하고 delete할 생각이다. 그래서 마지막 timeoutId가 있는데 이는 뒤에서 차근차근 설명하도록 하겠다.

 

필요한 변수를 선언하였으면 이제 함수를 만들어보자

 

 

이제 함수를 만들자

함수는 총 두가지를 만들 것이다.

  • typeText : 타이핑을 위한 함수
  • deleteText: 삭제를 위한 함수

 

이 함수들을 만들기 위해 우리는 한가지 생각을 해봐야한다.

어떻게 타이핑을 해야할 때인지, 삭제를 해야할 때인지를 알지?

이를 판단하기 위해서 우리는 index 변수를 사용할 것이다.

 

어떤 과정을 거치는지 자세히 한번 차근차근 적어보자

 

'안' 을 치고 index에 1을 더한다. -> index = 1
'녕' 을 추가한 뒤에 index에 1을 더한다 -> index = 2
...
'요'를 추가하고 index에 1을 더한다 -> index = 5
타이핑은 끝나고 삭제로 돌입한다.
'요'를 삭제하고 index에 1을 뺀다 -> index = 4
...
'안'을 삭제하고 index에 1을 뺀다 -> index = 0
삭제가 끝나고 타이핑에 돌입한다.
다음 content로 넘어간다 -> contentCount에 1을 더한다 -> contentCount = 1

 

 

여기서 우리는 깨달을 수 있다.

'안녕하세요'의 글자 수(5)가 index보다 큰 상태라면 '타이핑해야하는 상태'

index와 같아지는 순간에 '삭제해야하는 상태'

다시 index가 0이 되면 '타이핑해야하는 상태'

그리고 컨텐츠 카운트를 더해주면 된다

 

 

이 과정을 코드로 만들어보자

 

function deleteText() {
    if (timeoutId){
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      if (index > 0) {
        typedContent.textContent = typedContent.textContent.slice(0, -1);
        index--;
        deleteText();
      } else {
        setTimeout(() => {
          typeText();
        }, TYPED_ANIMATION_CONFIG.delayAfterDelete);
      }
    }, TYPED_ANIMATION_CONFIG.deleteSpeed);
  }

  function typeText() {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      if (index < TYPED_ANIMATION_CONFIG.texts[contentCount].length) {
        typedContent.textContent += TYPED_ANIMATION_CONFIG.texts[contentCount][index];
        index++;
        typeText();
      } else {
        setTimeout(() => {
          deleteText();
        }, TYPED_ANIMATION_CONFIG.delayAfterTyping);
        if (contentCount < TYPED_ANIMATION_CONFIG.texts.length - 1) {
          contentCount++;
        } else {
        // 마지막 컨텐츠가 완료되면 다시 처음 컨텐츠로 돌아간다
          contentCount = 0;
        }
      }
    }, TYPED_ANIMATION_CONFIG.typingSpeed);
  }

 

보면 알겠지만 각 함수안에 자기 자신이 들어가는 재귀 함수를 사용하였다.

typeText안에서 다음 번에 실행되어야 할 함수가 typeText인지 deleteText인지를 판단하기 때문에 이와 같은 형태로 구현을 완료하였다.

 

그리고 timeoutId를 여기서 쓰는데,

setTimeout 메서드 같은 경우, 몇초 후에 이 함수를 실행시킬지를 설정할 수 있다.

이미 함수 큐에 다음에 실행 될 함수가 남아있을 수 있는 경우, 예상치 못한 버그가 발생할 수 있기 때문에

clearTimeout으로 이미 실행 된 setTimeout 함수를 초기화함으로서 오류를 최소화 할 수 있다.

이것이 timeoutId를 사용한 이유.

자세한 것은 공식 문서를 참고하면 좋겠다.

https://developer.mozilla.org/ko/docs/Web/API/Window/setTimeout

 

setTimeout() 전역 함수 - Web API | MDN

전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 한 번 실행하는 타이머를 설정합니다.

developer.mozilla.org

 

 

이제 타이핑에 관한 함수는 끝났으니,

간단히 커서까지 깜빡이게 만들면 끝이다!

 

커서를 깜빡이게 만들어보자

커서를 깜빡이게 하는 것은 간단하다.

우선 cursorOn을 통해 커서를 사용할지 말지에 대해 결정하고,

cursorOn === true일 경우 커서를 생성하고 style을 입히면 된다.

무한으로 일정한 간격으로 계속 실행을 시킬 것이기 때문에 setInterval 내장함수를 사용하며

cursorSetting안에 blinkSpeed를 delay 시간에 넣는다.

그리고 cursor의 opacity를 0과 1로 왔다갔다 해주기만 하면 끝이다.

if (TYPED_ANIMATION_CONFIG.cursorSettings.cursorOn) {
    const cursor = document.createElement("div");
    cursor.className = "cursor";
    container.appendChild(cursor);

    setInterval(() => {
      cursor.style.opacity = cursor.style.opacity === "0" ? "1" : "0";  
    }, TYPED_ANIMATION_CONFIG.cursorSettings.blinkSpeed);
  }

 

자 그럼 모든 것이 완성되었다.

완성된 코드를 마지막으로 보자.

 

완성된 코드

// typed animation

// Typed Animation Settings
const TYPED_ANIMATION_CONFIG = {
  // 여기에서 설정을 변경하세요
  texts: ["안녕하세요", "뫙뭉 블로그입니다", "환영합니다"], // 표시할 텍스트 목록
  typingSpeed: 100,         // 타이핑 속도 (ms)
  deleteSpeed: 100,         // 삭제 속도 (ms)
  delayAfterTyping: 1000,   // 타이핑 후 대기 시간 (ms)
  delayAfterDelete: 500,    // 삭제 후 대기 시간 (ms)
  cursorSettings: {
    cursorOn: true,
    blinkSpeed: 500         // 커서 깜빡임 속도 (ms)
  }
};


const TYPED_ANIMATION_SELECTORS = {
  container: ".typed-container"  // 타이핑 애니메이션이 들어갈 컨테이너의 클래스명
};


function initTypedAnimation() {
  const container = document.querySelector(TYPED_ANIMATION_SELECTORS.container);
  if (!container) {
    console.error(`${TYPED_ANIMATION_SELECTORS.container} 요소를 찾을 수 없습니다.`);
    return;
  }

  let index = 0;
  let contentCount = 0;
  let timeoutId = null;

  // 타이핑될 텍스트를 표시할 요소 생성
  const typedContent = document.createElement("div");
  typedContent.className = "typed-content";
  container.appendChild(typedContent);

  
  function deleteText() {
    if (timeoutId){
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      if (index > 0) {
        typedContent.textContent = typedContent.textContent.slice(0, -1);
        index--;
        deleteText();
      } else {
        setTimeout(() => {
          typeText();
        }, TYPED_ANIMATION_CONFIG.delayAfterDelete);
      }
    }, TYPED_ANIMATION_CONFIG.deleteSpeed);
  }

  function typeText() {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      if (index < TYPED_ANIMATION_CONFIG.texts[contentCount].length) {
        typedContent.textContent += TYPED_ANIMATION_CONFIG.texts[contentCount][index];
        index++;
        typeText();
      } else {
        setTimeout(() => {
          deleteText();
        }, TYPED_ANIMATION_CONFIG.delayAfterTyping);
        if (contentCount < TYPED_ANIMATION_CONFIG.texts.length - 1) {
          contentCount++;
        } else {
          contentCount = 0;
        }
      }
    }, TYPED_ANIMATION_CONFIG.typingSpeed);
  }

  // 커서 요소 생성 및 깜빡임 애니메이션 설정

  if (TYPED_ANIMATION_CONFIG.cursorSettings.cursorOn) {
    const cursor = document.createElement("div");
    cursor.className = "cursor";
    container.appendChild(cursor);

    setInterval(() => {
      cursor.style.opacity = cursor.style.opacity === "0" ? "1" : "0";  
    }, TYPED_ANIMATION_CONFIG.cursorSettings.blinkSpeed);
  }

  // 애니메이션 시작
  typeText();
}

// 페이지 로드 시 애니메이션 초기화
document.addEventListener('DOMContentLoaded', initTypedAnimation);
// initTypedAnimation();

 

이렇게 변수를 넣어놓으면, 추후 내가 또는 다른 사람이 코드를 가져다 사용하는 경우

가장 상단의 변수값만 설정하여 여러가지 모습을 보여줄 수 있다.

 

 

마무으리

오랜만에 스타일 관련 코드를 적어본 것 같다.

원래 취직을 하기 전에 참 이러한 것들을 좋아했다. 리액트를 배우고 기능 개발을 하고, api로 데이터를 왔다갔다 하고, 렌더링 최적화를 하고, 실무에서는 정말 많은 것들을 해야하기 때문에, 멀어져 있었던 것 같다.

또다른 이유는 요즘은 워낙 이런저런 라이브러리가 많다. 편의성도 좋고 기능도 훨씬 많다.

나는 물론 이미 완성되어있는 멋진 코드들을 가져다 쓰는 것에 적극 찬성이다.

하지만 이렇게 가끔은 VanilaJS로 어렵진 않지만 어떻게 하면 좋을까 생각해보면서 만들어보는게 재밌는 것 같다.

장난감을 가지고 노는 느낌이랄까ㅎㅎ 프로젝트를 거창하게 하기에는 조금 부담이 되지 않는가? (나만 그런 것 같기도 하다)

어쨌든 다음에는 또 쉬우면서도 많이쓰면서도 유용해 보일 것들을 한번 가져와보겠다!!

 
반응형