글 목록 애니메이션 구현

24. 07. 06. (4개월 전)

TL;DR: Rauno의 프로젝트 페이지 렌더링 애니메이션을 구현한 과정을 소개하고 한글 처리, 코드 가독성, 그리고 자연스러운 애니메이션에 대한 고민을 공유합니다.

동기

인터랙션 디자인 기술을 공부하던 중, rauno프로젝트 페이지를 발견했습니다. 여기서 각 프로젝트를 렌더링할 때 사용되는 애니메이션 효과가 저에게 시각적으로 너무나 매력적으로 다가왔습니다.

이를 구현해보고 싶었고 이 애니메이션을 재현하는 과정과 그 과정에서 얻은 통찰, 그리고 직면한 도전 과제들을 공유하고자 합니다. 생각보다 디테일이 많이 들어간 애니메이션이었고 그만큼 구현하고 나서 해냈다는 보람이 커서 좋았습니다. :)

rauno's projects page animation
rauno의 projects 페이지 애니메이션

애니메이션 이해하기

이 애니메이션은 다음과 같이 작동합니다:

초기 상태

각 요소(제목, 설명, 선)가 왼쪽에서 오른쪽으로 점진적으로 나타납니다.

타이밍

모든 텍스트 변환 애니메이션이 동시에 시작되는 것처럼 보이며, 각 항목에 약간의 지연 시간이 추가되어 순차적으로 나타납니다.

문자별 변환

제목, 설명, 연도가 한 글자씩 나타나며, 실제 텍스트가 표시되기 전에 랜덤한 문자들이 보입니다.

첫 번째 시도

랜덤한 문자를 위해 jh3y-GRLKMPYGLYPHS 변수를 사용했습니다:

const GLYPHS = 'ラドクリフマラソンわたしワタシんょンョたばこタバコとうきょうトウキョウ0123456789±!@#$%^&*()_+ABCDEFGHIJKLMNOPQRSTUVWXYZ';

각 요소(제목, 설명, 연도)의 텍스트를 문자열 배열로 변환하고, 각 문자를 GLYPHS에서 무작위로 선택한 문자로 대체하는 과정을 각 프레임마다 반복했습니다. 하지만 결과적으로 나온 애니메이션은 부자연스러워 보였습니다:

애니메이션 개선

추가 연구 끝에 Shuffle Text Effect With jQuery에서 영감💡을 얻어 몇 가지 중요한 통찰을 얻었습니다.

자연스러운 애니메이션을 위한 조건

더 자연스러운 텍스트 변환을 위해:

  • 각 문자의 유형에 맞는 랜덤 문자를 생성합니다 (예: 영어 소문자는 영어 소문자로).
  • 공백이 아닌 문자만 변환합니다.
  • 적절한 반복 횟수와 프레임 속도를 설정합니다.

적절한 반복 횟수와 프레임 속도를 설정하기 위해 iterationsfps 옵션을 가지고 놀아볼 수 있는 Playground 페이지를 만들었습니다:

랜덤 한글 문자 생성하기

영어의 글자 수는 26개지만 한글은 가능한 조합이 11,172개로 매우 많아 랜덤한 한글 문자를 생성하는 방법을 고민했습니다. 일일이 한글 문자를 배열에 저장하는 것은 비효율적이라고 생각했고, 이 문제를 해결하기 위해 유니코드를 사용했습니다:

모든 문자는 유니코드로 표현될 수 있다는 점을 이용해 다음과 같이 랜덤 한글 문자를 생성할 수 있습니다:

String.fromCharCode(0xac00 + Math.floor(Math.random() * 11172));

이 코드는 유효한 유니코드 범위(AC00에서 D7A3) 내에서 무작위 한글 문자를 생성합니다.

💡
한글이 가지는 유니코드 값은 AC00부터 D7A3까지며, 총 11,172개의 코드로 모든 한글을 표현할 수 있습니다.

애니메이션 구현하기

이제 자연스럽게 변환되는 애니메이션을 구현해 보겠습니다.

문자 유형 정의:

type CharType = 'lowerCase' | 'upperCase' | 'digit' | 'symbol' | 'korean';

유형에 따라 랜덤 문자를 생성하는 함수 구현:

const charPools: Record<CharType, string> = {
  lowerCase: 'abcdefghijklmnopqrstuvwxyz',
  upperCase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  digit: '0123456789',
  symbol: '!@#$%^&*()_+-=[]{}|;:,.<>?',
  korean: '',
  space: ' ',
};
 
function getRandomCharacterForType(charType: CharType): string {
  const pool = charPools[charType];
 
  if (charType === 'korean') {
    return String.fromCharCode(0xac00 + Math.floor(Math.random() * 11172));
  }
 
  return pool[Math.floor(Math.random() * pool.length)] || '';
}

주요 애니메이션 함수 구현:

export function shuffleLetters(element: HTMLElement, config: ShuffleConfig = {}): () => void {
  const options = {
    iterations: 8,
    fps: 30,
    onComplete: () => {},
    ...config,
  };
 
  // 텍스트 내용 분석
  const text = element.textContent ?? '';
  const charsArray = Array.from(text);
  const charsTypes = charsArray.map(char => {
    if (/\s/.test(char)) return 'space';
    if (isKorean(char)) return 'korean';
    if (/[a-z]/.test(char)) return 'lowerCase';
    if (/[A-Z]/.test(char)) return 'upperCase';
    if (/[0-9]/.test(char)) return 'digit';
    return 'symbol';
  });
  const charsPositions = charsArray.reduce<number[]>((acc, char, index) => {
    if (!/\s/.test(char)) acc.push(index);
    return acc;
  }, []);
 
  element.textContent = '';
 
  // 재귀적 애니메이션 함수 구현
  const shuffle = (start: number): void => {
    // 문자를 랜덤한 것으로 대체
    // 결과 문자열을 요소에 적용
    // fps에 따라 다음 프레임 예약
  };
 
  // 애니메이션 시작  중지 함수 반환
  shuffle(-options.iterations);
 
  return () => {
    if (timeout) clearTimeout(timeout);
  };
}

결과 및 추가 개선 사항

결과적으로 얻은 애니메이션은 원본과 훨씬 더 유사해졌습니다:

추가적인 개선 사항:

  • useInView 훅을 사용하여 화면에 보이는 요소만 애니메이션 적용
  • 선에 scaleX 애니메이션을 적용하여 왼쪽에서 오른쪽으로 자연스럽게 늘어나도록 함
  • 부드러운 효과를 위해 opacity 값 조정
  • 자연스러움을 위해 iterationsfps 값을 미세 조정

코드 가독성 고려사항

요구사항을 코드로 구현할 때는 코드의 가독성과 유지보수성을 고려해야 합니다. 예를 들어, 배열에서 짝수를 필터링하고 그 합계를 구할 때 다음과 같은 접근 방식을 고려해볼 수 있습니다:

// 방식 1: 비트 연산자 사용해  줄로 작성
const calculateEvenSum = arr => arr.filter(num => !(num & 1)).reduce((acc, val) => acc + val, 0);
 
// 방식 2:  단계를 변수에 할당
const calculateEvenSum = numbers => {
  const evenNumbers = numbers.filter(function (number) {
    return number % 2 === 0;
  });
 
  const sum = evenNumbers.reduce(function (total, number) {
    return total + number;
  }, 0);
 
  return sum;
};

다소 극단적인 예시지만, 대부분의 개발자가 (둘 중에선)두 번째 방식을 선호할 것입니다.

첫 번째 방식은 간결하지만, 비트 연산자를 사용하고 있으며 자바스크립트 초보자는 코드의 의도를 바로 파악하기 어려울 수 있습니다.

두 번째 방식은 첫 번째에 비해 약간 더 길지만, 코드의 의도와 동작을 명확하게 전달합니다. 각 단계가 분리되어 있어 로직을 따라가기 쉽고 기본적인 JS 문법만 사용해 초보자도 이해하기 쉽죠.

결론

rauno의 텍스트 애니메이션 효과를 재현하는 과정이 재밌었습니다. 자연스러운 문자 전환을 위한 방법을 연구하고 노력했습니다. 결과적으로, rauno의 프로젝트 페이지와 비슷한 애니메이션을 구현할 수 있었습니다.

참고 자료