타이핑효과를 구현해보자 (feat. Typed에서는 사람처럼 타이핑을 어떻게 만들까?)

들어가며
최근 회사에서 육성관련 키우기 게임을 맡게되어 작업하는 도중 캐릭터가 말하는 듯한 UI 관련 요구사항이 있었습니다. 관련하여 프로토타이핑 작업과정 부터 typed라이브러리를 사용 및 구현방법까지 확인해본 과정을 간단하게 공유해보려 합니다.
1. 말풍선에서는 타이핑 효과가 있어야 좋지요.!
이번에 작업한 키유기류 게임에서는 아래 첨부한 이미지와 같이 캐릭터가 유저에게 말하는 것처럼 말풍선형태의 UI가 존재하였습니다. 이런 UI구조에서는 보다 효과적인건 정적으로 텍스트가 한번에 뜨는것보다는 타이핑하듯이 텍스트를 노출시켜주면 보다 유저경험에 좋다고 생각하기때문에 관련 기능을 어떻게 만들지 생각해보았습니다.
2. 간단한 프로토타이핑 작성
우선 생각나는 대로 타이핑 효과를 구현해보았습니다. 말풍선에 입력할 타이핑메세지와 타이핑 속도를 인자로 받아 해당 타이핑속도가 지날때마다 한글자씩 추가 노출을 시키는 간단한 타이핑 구현체 입니다.
/** * @description * setTimeout을 보다 선언적으로 사용하기 위한 util함수 */ const delay = (ms: number) => { return new Promise<void>((resolve) => setTimeout(resolve, ms)); } interface Props { // 말풍선에 작성할 텍스트를 입력합니다. text: string; // 타이핑 속도를 지정합니다. typing_speed?: number; } const SpeechBubble: FC<Props> = ({ text, typing_speed = 10, }) => { const text_ref = useRef<HTMLSpanElement>(null); useEffect(() => { const typing = async () => { const letter = text.split(''); while (letter.length) { await delay(typing_speed); if (text_ref.current) { text_ref.current.innerHTML += letter.shift(); } } }; typing(); }, [text, typing_speed]); return ( <div className='bubble'> <span className='text' ref={text_ref} > </div> ); };
위의 코드를 토대로 구현후 나온 결과물은 아래와 같습니다. 말풍선 타이핑 효과가 잘 보이는것 같아요..!
3. 어딘가 살짝 아쉬운데..
위의 영상을 자세히 보면 어딘가 이상하게 어색한걸 볼 수 있습니다. 그 이유는 고정된 typing_speed마다 한글자씩 추가되기때문에 사람이 타이핑효과를 주는것보단 메크로를 통해 나온것 같은 어색함을 볼수 있어요. 때문에 관련해서 이미 잘 사용중인 라이브러리가 있는지 검색하던 중 typed
라는것을 알게되었습니다.
4. Typed?
typed
라이브러리는 텍스트 타이핑에 대한 여러가지 효과를 구현한 라이브러리 입니다. 사람이 타이핑 치는듯한 효과, 백스페이스 기능, 커서 효과 등 여러 기능들을 합니다. 이번글에서는 타이핑주제에 해당하는 타이핑 효과만 중점적으로 보도록 하겠습니다.
위의 영상을 보면 처음 작성한 예시보다 훨씬 사람이 타이핑한듯한 효과가 강조되어 보입니다. 보다 자세한 예시는 Typed 에서 확인하실 수 있어요.~
5. Typed 작성 예시
js 베이스이긴 하지만 React에서도 작성자체는 상당히 간단합니다.
우선 ref를 한개 생성후 Typed의 첫번째 인자에 효과를 넣어야 하는 dom의 참조ref를 넣어주고 두번째 인자로는 타이핑문구, 커서 노출 등의 옵션을 설정합니다. 아래 예시는 처음 예시코드를 Typed로 작성한 예시 입니다.
interface Props { // 말풍선에 작성할 텍스트를 입력합니다. text: string; // 타이핑 속도를 지정합니다. typing_speed?: number; } const SpeechBubble: FC<Props> = ({ text, typing_speed = 10, }) => { const text_ref = useRef<HTMLSpanElement>(null); const typed_ref = useRef<Typed>(); useEffect(() => { typed_ref.current = new Typed(text_ref.current, { strings: [text.replace(/\n/g, '<br />')], showCursor: false, typeSpeed: typing_speed, }); return () => { typed_ref.current?.destroy(); }; }, [text, typing_speed]); return ( <div className='bubble'> <span className='text' ref={text_ref} > </div> ); };
아래 영상을 보시면 두개(프로토타이핑버전, Typed적용 버전) 다 같은 타이핑속도를 각각 준 예시영상입니다.
윗 영상이 프로토타이핑 버전이고 아래 영상이 typed를 사용한 타이핑 예시입니다. typed를 사용한 예시가 훨씬 자연스러워 보이는 타이핑 효과를 주는것을 알 수 있습니다.
6. Typed에서는 어떻게 자연스러운 효과를 만드는걸까..?
Typed
를 사용한 예시에서도 분명 동일한 speed인자를 넣었는데 어떻게 Typed
에서만 진짜 사람이 타이핑하듯 구현한건지 이부분이 너무 궁금하여 Typed
를 뜯어보기로 결정하였습니다. 땅땅!!
7. Typed의 구성 요소
typed의 라이브러리를 확인해보면 아래와 같이 4가지의 파일만이 존재하는것을 확인할 수 있습니다.
- defaults.js // lib 구성하는 options의 default 값들 선언 - initializer.js // defaults 값과 lib에 넣은 값을 합쳐서 load시점의 initial 하는 부분 - html-parser.js // html 로 된 부분 파싱하기 위한 utils 영역 - typed.js // 실제 구현화 js
여러 효과중에서도 사람처럼 나오는 타이핑 효과를 확인해 보도록 합시다.
// typed.js 에서 타이핑 부분만 축약해둔 코드입니다 export default class Typed { constructor(elementId, options) { // props로 받은 인자를 기반으로 각종 props 초기화세팅 initializer.load(this, options, elementId); // 타이핑 함수 시작 this.begin(); } begin() { ... // 타이핑 작성 시작 this.typewrite(this.strings[this.sequence[this.arrayPos]], this.strPos); } typewrite(curString, curStrPos) { ... // typeSpeed를 통해 humanizer반환 (사람타이핑 속도 흉내) const humanize = this.humanizer(this.typeSpeed); let numChars = 1; setTimeout(() => ... // humanize 시간이 지날때마다 한글자식 타이핑 추가 if (curStrPos >= curString.length) { this.doneTyping(curString, curStrPos); } else { this.keepTyping(curString, curStrPos, numChars); } ... , humanize); } // 가장 중요포인트 별두개 돼지꼬리 땅땅 humanizer(speed) { // speed ≤ 반환값 ≤ speed * 1.5 // 100ms 인 경우 100~150사이의 ms 딜레이 후 노출 return Math.round((Math.random() * speed) / 2) + speed; } }
실제 구현부는 다양한 기능들의 지원을 위해 훨씬 복잡하지만 사람처럼 타이핑 하는 가장 큰 핵심 구현포인트는 humanizer
함수 입니다.
해당 함수를 통해 한글자가 추가 타이핑될때마다 딜레이를 조금씩 다르게 주어 (최대 1.5배) 보다 사람처럼 타이핑 하는 효과를 만들어 내게 됩니다. 단순 로직자체만으로는 상당히 심플합니다. 이제 이 구현포인트를 아까 작성한 예시에 대입해서 구현해 보도록 하겠습니다.
코드 구현
/** * @description * setTimeout을 보다 선언적으로 사용하기 위한 util함수 */ const delay = (ms: number) => { return new Promise<void>((resolve) => setTimeout(resolve, ms)); } interface Props { // 말풍선에 작성할 텍스트를 입력합니다. text: string; // 타이핑 속도를 지정합니다. typing_speed?: number; } const SpeechBubble: FC<Props> = ({ text, typing_speed = 10, }) => { const text_ref = useRef<HTMLSpanElement>(null); useEffect(() => { const typing = async () => { const letter = text.split(''); while (letter.length) { // 변경포인트 -> 단순한 동일한 속도가 아닌 랜덤하게 속도를 조정합니다. await delay(humanizer(typing_speed)); if (text_ref.current) { text_ref.current.innerHTML += letter.shift(); } } }; typing(); }, [text, typing_speed]); return ( <div className='bubble'> <span className='text' ref={text_ref} > </div> ); }; function humanizer(speed: number) { // speed ≤ 반환값 ≤ speed * 1.5 // 100ms 인 경우 100~150사이의 ms 딜레이 후 노출 return Math.round((Math.random() * speed) / 2) + speed; }
8. 결론
사람처럼 타이핑을 어떻게 하지라는 생각에 포커스를 가지고 너무 어렵게만 생각했는데 실제 구현부는 생각보다 훨씬 심플하지만 강력한 효과를 보이는것을 알 수 있었습니다. 또한 이렇게 궁금한 파트에 있어 라이브러리를 분석하는것도 이번 예시를 보시며 분석하며 성장하는 포인트를 느끼셨으면 하는 마음에 이 글을 작성합니다.