완성된 작업물은 여기!
GitHub - SIMHANSOL/Simhansol-Box: 심한솔 박스
심한솔 박스. Contribute to SIMHANSOL/Simhansol-Box development by creating an account on GitHub.
github.com
사실 한글 타이핑은 이미 라이브러리가 존재한다.
근데 라이브러리만 사용하면 재미가 없으니 직접 만들어보며 원리를 깨달아보고자 했다.

일단 가장 먼저 React 환경에서 만들어볼 것이기 때문에 컴포넌트로 개발할 예정이다.
컴포넌트에서는 문자열 타입의 children props을 전달받아 children의 문자열을 천천히 타이핑하는 기능을 담당할 것이다.
// how to use
<TypingEffect>타이핑 예제입니다.</TypingEffect>
먼저 구현을 하기 위해서는 한글과 영어의 특성을 잘 알고 있어야 한다.
영어같은 경우는 "Apple is iphone" 문자열의 타이핑 효과를 내기 위해서는 순차적으로 표시만 해주면 별다른 세팅없이 끝나지만 한글은 영어처럼 쉽지 않다.
모음과 자음이 복잡하게 연결되어 하나의 글자를 나타낸다.
그리고 이 문자를 어떻게 표기하고 있는지 유니코드 지식을 갖추고 있어야 한다.

그래서 가장 먼저 타이핑 효과를 내기 위해서 타이핑할 문자열을 분해해버리도록 하였다.
분해한 다음에 천천히 재조립을 하면 타이핑 효과를 낼 수 있겠단 생각을 했다.
현대 한글의 음절
초성: ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ (19개)
중성: ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ (21개)
종성: ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ (27개)
위의 초성, 중성, 종성을 조합해서 만들 수 있는 글자 수는 다음과 같다. 19 × 21 × ( 27 + 1 ) = 11172
일단 초성, 중성, 종성으로 분해를 해버리기 위해 타입을 정의했다.
// type.ts
export const chosung = [
'ㄱ',
'ㄲ',
'ㄴ',
'ㄷ',
'ㄸ',
'ㄹ',
'ㅁ',
'ㅂ',
'ㅃ',
'ㅅ',
'ㅆ',
'ㅇ',
'ㅈ',
'ㅉ',
'ㅊ',
'ㅋ',
'ㅌ',
'ㅍ',
'ㅎ',
];
export const jungsung = [
'ㅏ',
'ㅐ',
'ㅑ',
'ㅒ',
'ㅓ',
'ㅔ',
'ㅕ',
'ㅖ',
'ㅗ',
'ㅘ',
'ㅙ',
'ㅚ',
'ㅛ',
'ㅜ',
'ㅝ',
'ㅞ',
'ㅟ',
'ㅠ',
'ㅡ',
'ㅢ',
'ㅣ',
];
export const jongsung = [
'',
'ㄱ',
'ㄲ',
'ㄳ',
'ㄴ',
'ㄵ',
'ㄶ',
'ㄷ',
'ㄹ',
'ㄺ',
'ㄻ',
'ㄼ',
'ㄽ',
'ㄾ',
'ㄿ',
'ㅀ',
'ㅁ',
'ㅂ',
'ㅄ',
'ㅅ',
'ㅆ',
'ㅇ',
'ㅈ',
'ㅊ',
'ㅋ',
'ㅌ',
'ㅍ',
'ㅎ',
];
"안" 문자를 분해하면 "ㅇㅏㄴ"이다. 초성은 "ㅇ", 중성은 "ㅏ", 종성은 "ㄴ"이다. |
"가" 문자를 분해하면 "가"이다. 초성은 "ㄱ", 중성은 "ㅏ", 종성은 없다. |
이처럼 나는 초성, 중성, 종성을 이용해 문자열을 분해하고 천천히 조립하여 타이핑 효과를 내고자 했다.
근데 문제는 "안" 이라는 글자를 어떻게 코드 상에서 분해를 할 수 있는지에 대해서는 정작 고민해보지 않았던 것이다.

다행히도 검색 결과,
한글 유니코드에 대한 규칙을 상세하게 설명해주고 있는 블로그를 찾았고 많은 도움이 되었다.
도움이 됐을 뿐만 아니라 아예 초성, 중성, 종성을 통해 어떻게 조합하고 분해하는지 로직 자체를 알려주고 있다.

이에 참고하여 함수를 작성하기 시작했다.
// function.ts
import { chosung, jongsung, jungsung } from './type';
function isHangul(charCode: number) {
if (charCode >= 0xac00 && charCode <= 0xd7a3) return true;
return false;
}
function combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice,
}: {
returnText: string[];
cho: number;
jung: number;
jong: number;
index: number;
splice: number;
}) {
return returnText.splice(
index,
splice,
String.fromCharCode(0xac00 + cho * Math.floor(0xae4c - 0xac00) + jung * Math.floor(0xac1c - 0xac00) + jong)
);
}
export function decomposeText(text: string) {
let returnText = '';
for (let i = 0; i < text.length; i += 1) {
const char = text.charAt(i);
const code = char.charCodeAt(0);
if (isHangul(code)) {
const currentHangul = char.charCodeAt(0) - 0xac00;
const cho = Math.floor(currentHangul / 21 / 28);
const jung = Math.floor(currentHangul / 28) % 21;
const jong = currentHangul % 28;
returnText += `${chosung[cho]}${jungsung[jung]}${jongsung[jong]}`;
} else {
returnText += char;
}
}
return returnText;
}
export function combineText(text: string) {
let cho = -1;
let jung = -1;
let jong = -1;
let decompose = '';
let currentText = '';
let nextText = '';
let index;
for (index = 0; index < text.length; index += 1) {
currentText = text.charAt(index);
nextText = text.charAt(index + 1);
decompose = decomposeText(currentText);
const tempCho = decompose.charAt(0);
const tempJung = decompose.charAt(1);
const tempJong = decompose.charAt(2);
if (
// 한글 타이핑이 가능한 경우 INDEX 반환 후 반복문 종료
!(
(chosung.includes(currentText) && chosung.includes(nextText)) || // 자음 + 자음
(tempCho !== '' && tempJung !== '' && tempJong !== '') || // 자음 + 모음 + 자음
(tempCho !== '' && tempJung !== '' && tempJong === '' && chosung.indexOf(nextText) === -1) || // 자음 + 모음 + 자음이 아닌 문자
currentText === ' ' || // 공백
nextText === ' ' || // 공백
!(
// 한글이 아닌 문자
(
isHangul(text.charCodeAt(index)) ||
chosung.includes(currentText) ||
jongsung.includes(currentText) ||
jungsung.includes(currentText)
)
)
)
) {
cho = chosung.indexOf(tempCho || currentText);
jung = jungsung.indexOf(tempJung);
jong = jongsung.indexOf(tempJong);
break;
}
}
const nextHangul = nextText || currentText;
if (nextHangul === '') return text;
const returnText = [...text];
if (jungsung.includes(currentText)) {
const prevDecompose = decomposeText(text.charAt(index - 1));
cho = chosung.indexOf(prevDecompose.charAt(0));
jung = jungsung.indexOf(prevDecompose.charAt(1));
combineCharCode({
returnText,
cho,
jung,
jong: 0,
index: index - 1,
splice: 1,
});
cho = chosung.indexOf(prevDecompose.charAt(2));
jung = jungsung.indexOf(currentText);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 1,
});
} else if (jungsung.includes(nextHangul)) {
jung = jungsung.indexOf(nextHangul);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 2,
});
} else if (jungsung.indexOf(decompose.charAt(decompose.length - 1)) !== -1) {
cho = chosung.indexOf(decompose.charAt(0));
jung = jungsung.indexOf(decompose.charAt(decompose.length - 1));
jong = jongsung.indexOf(nextHangul);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 2,
});
}
return returnText.join('');
}
function decomposeText(text: string)
input | return |
"안녕하세요." | "ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ. |
이 함수는 매개 변수 text에 들어온 문자열을 초성, 중성, 종성으로 분해하는 기능을 하고 있다.
조건문을 통해 한글이면 분해하고 그렇지 않으면 문자 그대로 반환한다.
function combileText(text: string)
input | return |
"ㅇㅏㄴㅕㅇ" | "안녕" |
이 함수는 매개 변수 text에 들어온 분해된 문자열을 다시 재조립하는데 사용한다.
로직 자체는 한눈에 이해하기 힘들지만 간단히 설명하면 첫 문자를 꺼내와서 초성이면 다음 문자에 중성이 있는지 확인하고 이 다음 종성이 있으면 하나의 글자로 조립하는 형태이다. 당연히 중성이나 종성이 없다면 하나의 글자로 조립한다. 이를 문자열이 끝날 때까지 반복한다.

이제 이 함수를 통해 분해하고, 조립할 수 있게 되었다.
남은 건 컴포넌트 상에서 props의 children으로 받아온 텍스트를 decomposeText 함수로 분해하고
특정 시간마다 글자 하나 하나 추가하면서 combineText 함수에 집어넣으면 된다.
이렇게 되면 타이핑 효과처럼 한글이 차차 작성되어 나가는 걸 볼 수 있다.
커서 효과도 넣고, 옵저버를 통해 화면 상에 보일 때만 타이핑 효과가 진행되도록 만들었다.
옵저버를 넣은 이유는 타이핑 효과를 받는 엘리먼트가 화면 밖에 있을 때 보이기도 전에 글자가 완성되어 있는 것을 방지하고자 넣어뒀다.
아래 코드는 mui를 함께 사용하고 있어서 mui에 맞게 작성했다.
// typing-effect.tsx
import type { TypographyProps } from '@mui/material';
import { useCallback, useEffect, useRef, useState } from 'react';
import { observer as observerFunction } from '../../../modules';
import { combineText, decomposeText } from './function';
import { Cursor, TypingWrapper } from './style';
export interface TypingEffectProps extends TypographyProps {
children: string;
duration?: number;
delay?: number;
observer?: boolean;
}
export default function TypingEffect(props: TypingEffectProps) {
const { children, duration = 50, delay = 300, observer, ...restProps } = props;
const [typedText, setTypedText] = useState('');
const [textIndex, setTextIndex] = useState(0);
const elementRef = useRef<HTMLParagraphElement>(null);
const typingText = useRef(decomposeText(children));
const play = useCallback(
() =>
setTimeout(
() => {
if (textIndex === typingText.current.length) return;
setTypedText((prev) => combineText(prev + typingText.current[textIndex]));
setTextIndex((prev) => prev + 1);
},
textIndex === 0 ? delay : duration
),
[delay, duration, textIndex]
);
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | undefined;
let intersectionObserver: IntersectionObserver;
if (observer != null && elementRef.current != null) {
intersectionObserver = observerFunction(
elementRef.current,
() => {
timer = play();
},
{ callbackOnce: true }
);
} else {
timer = play();
}
return () => {
clearTimeout(timer);
if (intersectionObserver != null) {
intersectionObserver.disconnect();
}
};
}, [observer, play]);
return (
<>
<TypingWrapper
ref={elementRef}
{...restProps}>
{typedText}
</TypingWrapper>
<Cursor>|</Cursor>
</>
);
}
// style.tsx
import styled from '@emotion/styled';
import { Box, Typography, keyframes } from '@mui/material';
import theme from '../../../styles/theme';
export const blink = keyframes`
from {
opacity: 1;
}
to {
opacity: 0;
}
`;
export const TypingWrapper = styled(Typography)`
display: inline-block;
`;
export const Cursor = styled(Box)`
display: inline-block;
animation: ${blink} 1s infinite;
color: ${theme.palette.grey[900]};
animation-timing-function: steps(2, jump-none);
`;
// observer.ts
export interface ObserverOptions {
root?: HTMLElement;
rootMargin?: string;
threshold?: number;
callbackOnce?: boolean;
}
export default function Observer(
elements: HTMLElement[] | HTMLElement,
callback: (_element: Element, _observer: IntersectionObserver) => void,
options?: ObserverOptions
) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(({ isIntersecting, target }) => {
if (isIntersecting) {
callback(target, observer);
if (options != null && options.callbackOnce) {
observer.unobserve(target);
}
}
});
}, options);
if (elements instanceof Array) {
elements.forEach((element) => observer.observe(element));
} else {
observer.observe(elements);
}
return observer;
}

커서 효과 넣기 전에 만든 이미지라 예제에는 커서가 보이지 않지만 실제로는 타이핑되는 글자에 따라 깜빡인다.
'개발 이야기 > React' 카테고리의 다른 글
React, Key 속성을 왜 사용하는지 알고 사용하자 (0) | 2023.07.19 |
---|---|
React(리액트)는 Storybook(스토리북)을 적극 사용하자. (0) | 2023.07.05 |
육각형 그래프 애니메이션을 구현해보았다. (0) | 2023.06.03 |
완성된 작업물은 여기!
GitHub - SIMHANSOL/Simhansol-Box: 심한솔 박스
심한솔 박스. Contribute to SIMHANSOL/Simhansol-Box development by creating an account on GitHub.
github.com
사실 한글 타이핑은 이미 라이브러리가 존재한다.
근데 라이브러리만 사용하면 재미가 없으니 직접 만들어보며 원리를 깨달아보고자 했다.

일단 가장 먼저 React 환경에서 만들어볼 것이기 때문에 컴포넌트로 개발할 예정이다.
컴포넌트에서는 문자열 타입의 children props을 전달받아 children의 문자열을 천천히 타이핑하는 기능을 담당할 것이다.
// how to use
<TypingEffect>타이핑 예제입니다.</TypingEffect>
먼저 구현을 하기 위해서는 한글과 영어의 특성을 잘 알고 있어야 한다.
영어같은 경우는 "Apple is iphone" 문자열의 타이핑 효과를 내기 위해서는 순차적으로 표시만 해주면 별다른 세팅없이 끝나지만 한글은 영어처럼 쉽지 않다.
모음과 자음이 복잡하게 연결되어 하나의 글자를 나타낸다.
그리고 이 문자를 어떻게 표기하고 있는지 유니코드 지식을 갖추고 있어야 한다.

그래서 가장 먼저 타이핑 효과를 내기 위해서 타이핑할 문자열을 분해해버리도록 하였다.
분해한 다음에 천천히 재조립을 하면 타이핑 효과를 낼 수 있겠단 생각을 했다.
현대 한글의 음절
초성: ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ (19개)
중성: ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ (21개)
종성: ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ (27개)
위의 초성, 중성, 종성을 조합해서 만들 수 있는 글자 수는 다음과 같다. 19 × 21 × ( 27 + 1 ) = 11172
일단 초성, 중성, 종성으로 분해를 해버리기 위해 타입을 정의했다.
// type.ts
export const chosung = [
'ㄱ',
'ㄲ',
'ㄴ',
'ㄷ',
'ㄸ',
'ㄹ',
'ㅁ',
'ㅂ',
'ㅃ',
'ㅅ',
'ㅆ',
'ㅇ',
'ㅈ',
'ㅉ',
'ㅊ',
'ㅋ',
'ㅌ',
'ㅍ',
'ㅎ',
];
export const jungsung = [
'ㅏ',
'ㅐ',
'ㅑ',
'ㅒ',
'ㅓ',
'ㅔ',
'ㅕ',
'ㅖ',
'ㅗ',
'ㅘ',
'ㅙ',
'ㅚ',
'ㅛ',
'ㅜ',
'ㅝ',
'ㅞ',
'ㅟ',
'ㅠ',
'ㅡ',
'ㅢ',
'ㅣ',
];
export const jongsung = [
'',
'ㄱ',
'ㄲ',
'ㄳ',
'ㄴ',
'ㄵ',
'ㄶ',
'ㄷ',
'ㄹ',
'ㄺ',
'ㄻ',
'ㄼ',
'ㄽ',
'ㄾ',
'ㄿ',
'ㅀ',
'ㅁ',
'ㅂ',
'ㅄ',
'ㅅ',
'ㅆ',
'ㅇ',
'ㅈ',
'ㅊ',
'ㅋ',
'ㅌ',
'ㅍ',
'ㅎ',
];
"안" 문자를 분해하면 "ㅇㅏㄴ"이다. 초성은 "ㅇ", 중성은 "ㅏ", 종성은 "ㄴ"이다. |
"가" 문자를 분해하면 "가"이다. 초성은 "ㄱ", 중성은 "ㅏ", 종성은 없다. |
이처럼 나는 초성, 중성, 종성을 이용해 문자열을 분해하고 천천히 조립하여 타이핑 효과를 내고자 했다.
근데 문제는 "안" 이라는 글자를 어떻게 코드 상에서 분해를 할 수 있는지에 대해서는 정작 고민해보지 않았던 것이다.

다행히도 검색 결과,
한글 유니코드에 대한 규칙을 상세하게 설명해주고 있는 블로그를 찾았고 많은 도움이 되었다.
도움이 됐을 뿐만 아니라 아예 초성, 중성, 종성을 통해 어떻게 조합하고 분해하는지 로직 자체를 알려주고 있다.

이에 참고하여 함수를 작성하기 시작했다.
// function.ts
import { chosung, jongsung, jungsung } from './type';
function isHangul(charCode: number) {
if (charCode >= 0xac00 && charCode <= 0xd7a3) return true;
return false;
}
function combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice,
}: {
returnText: string[];
cho: number;
jung: number;
jong: number;
index: number;
splice: number;
}) {
return returnText.splice(
index,
splice,
String.fromCharCode(0xac00 + cho * Math.floor(0xae4c - 0xac00) + jung * Math.floor(0xac1c - 0xac00) + jong)
);
}
export function decomposeText(text: string) {
let returnText = '';
for (let i = 0; i < text.length; i += 1) {
const char = text.charAt(i);
const code = char.charCodeAt(0);
if (isHangul(code)) {
const currentHangul = char.charCodeAt(0) - 0xac00;
const cho = Math.floor(currentHangul / 21 / 28);
const jung = Math.floor(currentHangul / 28) % 21;
const jong = currentHangul % 28;
returnText += `${chosung[cho]}${jungsung[jung]}${jongsung[jong]}`;
} else {
returnText += char;
}
}
return returnText;
}
export function combineText(text: string) {
let cho = -1;
let jung = -1;
let jong = -1;
let decompose = '';
let currentText = '';
let nextText = '';
let index;
for (index = 0; index < text.length; index += 1) {
currentText = text.charAt(index);
nextText = text.charAt(index + 1);
decompose = decomposeText(currentText);
const tempCho = decompose.charAt(0);
const tempJung = decompose.charAt(1);
const tempJong = decompose.charAt(2);
if (
// 한글 타이핑이 가능한 경우 INDEX 반환 후 반복문 종료
!(
(chosung.includes(currentText) && chosung.includes(nextText)) || // 자음 + 자음
(tempCho !== '' && tempJung !== '' && tempJong !== '') || // 자음 + 모음 + 자음
(tempCho !== '' && tempJung !== '' && tempJong === '' && chosung.indexOf(nextText) === -1) || // 자음 + 모음 + 자음이 아닌 문자
currentText === ' ' || // 공백
nextText === ' ' || // 공백
!(
// 한글이 아닌 문자
(
isHangul(text.charCodeAt(index)) ||
chosung.includes(currentText) ||
jongsung.includes(currentText) ||
jungsung.includes(currentText)
)
)
)
) {
cho = chosung.indexOf(tempCho || currentText);
jung = jungsung.indexOf(tempJung);
jong = jongsung.indexOf(tempJong);
break;
}
}
const nextHangul = nextText || currentText;
if (nextHangul === '') return text;
const returnText = [...text];
if (jungsung.includes(currentText)) {
const prevDecompose = decomposeText(text.charAt(index - 1));
cho = chosung.indexOf(prevDecompose.charAt(0));
jung = jungsung.indexOf(prevDecompose.charAt(1));
combineCharCode({
returnText,
cho,
jung,
jong: 0,
index: index - 1,
splice: 1,
});
cho = chosung.indexOf(prevDecompose.charAt(2));
jung = jungsung.indexOf(currentText);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 1,
});
} else if (jungsung.includes(nextHangul)) {
jung = jungsung.indexOf(nextHangul);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 2,
});
} else if (jungsung.indexOf(decompose.charAt(decompose.length - 1)) !== -1) {
cho = chosung.indexOf(decompose.charAt(0));
jung = jungsung.indexOf(decompose.charAt(decompose.length - 1));
jong = jongsung.indexOf(nextHangul);
combineCharCode({
returnText,
cho,
jung,
jong,
index,
splice: 2,
});
}
return returnText.join('');
}
function decomposeText(text: string)
input | return |
"안녕하세요." | "ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ. |
이 함수는 매개 변수 text에 들어온 문자열을 초성, 중성, 종성으로 분해하는 기능을 하고 있다.
조건문을 통해 한글이면 분해하고 그렇지 않으면 문자 그대로 반환한다.
function combileText(text: string)
input | return |
"ㅇㅏㄴㅕㅇ" | "안녕" |
이 함수는 매개 변수 text에 들어온 분해된 문자열을 다시 재조립하는데 사용한다.
로직 자체는 한눈에 이해하기 힘들지만 간단히 설명하면 첫 문자를 꺼내와서 초성이면 다음 문자에 중성이 있는지 확인하고 이 다음 종성이 있으면 하나의 글자로 조립하는 형태이다. 당연히 중성이나 종성이 없다면 하나의 글자로 조립한다. 이를 문자열이 끝날 때까지 반복한다.

이제 이 함수를 통해 분해하고, 조립할 수 있게 되었다.
남은 건 컴포넌트 상에서 props의 children으로 받아온 텍스트를 decomposeText 함수로 분해하고
특정 시간마다 글자 하나 하나 추가하면서 combineText 함수에 집어넣으면 된다.
이렇게 되면 타이핑 효과처럼 한글이 차차 작성되어 나가는 걸 볼 수 있다.
커서 효과도 넣고, 옵저버를 통해 화면 상에 보일 때만 타이핑 효과가 진행되도록 만들었다.
옵저버를 넣은 이유는 타이핑 효과를 받는 엘리먼트가 화면 밖에 있을 때 보이기도 전에 글자가 완성되어 있는 것을 방지하고자 넣어뒀다.
아래 코드는 mui를 함께 사용하고 있어서 mui에 맞게 작성했다.
// typing-effect.tsx
import type { TypographyProps } from '@mui/material';
import { useCallback, useEffect, useRef, useState } from 'react';
import { observer as observerFunction } from '../../../modules';
import { combineText, decomposeText } from './function';
import { Cursor, TypingWrapper } from './style';
export interface TypingEffectProps extends TypographyProps {
children: string;
duration?: number;
delay?: number;
observer?: boolean;
}
export default function TypingEffect(props: TypingEffectProps) {
const { children, duration = 50, delay = 300, observer, ...restProps } = props;
const [typedText, setTypedText] = useState('');
const [textIndex, setTextIndex] = useState(0);
const elementRef = useRef<HTMLParagraphElement>(null);
const typingText = useRef(decomposeText(children));
const play = useCallback(
() =>
setTimeout(
() => {
if (textIndex === typingText.current.length) return;
setTypedText((prev) => combineText(prev + typingText.current[textIndex]));
setTextIndex((prev) => prev + 1);
},
textIndex === 0 ? delay : duration
),
[delay, duration, textIndex]
);
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | undefined;
let intersectionObserver: IntersectionObserver;
if (observer != null && elementRef.current != null) {
intersectionObserver = observerFunction(
elementRef.current,
() => {
timer = play();
},
{ callbackOnce: true }
);
} else {
timer = play();
}
return () => {
clearTimeout(timer);
if (intersectionObserver != null) {
intersectionObserver.disconnect();
}
};
}, [observer, play]);
return (
<>
<TypingWrapper
ref={elementRef}
{...restProps}>
{typedText}
</TypingWrapper>
<Cursor>|</Cursor>
</>
);
}
// style.tsx
import styled from '@emotion/styled';
import { Box, Typography, keyframes } from '@mui/material';
import theme from '../../../styles/theme';
export const blink = keyframes`
from {
opacity: 1;
}
to {
opacity: 0;
}
`;
export const TypingWrapper = styled(Typography)`
display: inline-block;
`;
export const Cursor = styled(Box)`
display: inline-block;
animation: ${blink} 1s infinite;
color: ${theme.palette.grey[900]};
animation-timing-function: steps(2, jump-none);
`;
// observer.ts
export interface ObserverOptions {
root?: HTMLElement;
rootMargin?: string;
threshold?: number;
callbackOnce?: boolean;
}
export default function Observer(
elements: HTMLElement[] | HTMLElement,
callback: (_element: Element, _observer: IntersectionObserver) => void,
options?: ObserverOptions
) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(({ isIntersecting, target }) => {
if (isIntersecting) {
callback(target, observer);
if (options != null && options.callbackOnce) {
observer.unobserve(target);
}
}
});
}, options);
if (elements instanceof Array) {
elements.forEach((element) => observer.observe(element));
} else {
observer.observe(elements);
}
return observer;
}

커서 효과 넣기 전에 만든 이미지라 예제에는 커서가 보이지 않지만 실제로는 타이핑되는 글자에 따라 깜빡인다.
'개발 이야기 > React' 카테고리의 다른 글
React, Key 속성을 왜 사용하는지 알고 사용하자 (0) | 2023.07.19 |
---|---|
React(리액트)는 Storybook(스토리북)을 적극 사용하자. (0) | 2023.07.05 |
육각형 그래프 애니메이션을 구현해보았다. (0) | 2023.06.03 |