๋ฐ˜์‘ํ˜•

์ด๋ฏธ์ง€ ์ถœ์ฒ˜ dev.to

 

๊ธฐ๋ณธ ๋กœ์ง


ํƒ€์ž๊ธฐ๋กœ ํ•œ ๊ธ€์ž์”ฉ ์ž…๋ ฅํ•˜๋Š” ํšจ๊ณผ(Typewriter Effect)๋Š” ์ด๋ฏธ ์ˆ˜ ๋งŽ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์ง€๋งŒ, setInterval ํƒ€์ด๋จธ API๋ฅผ ์ด์šฉํ•ด์„œ ์ง์ ‘ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ์›๋ž˜ ๋ฌธ์žฅ(ํ…์ŠคํŠธ)์„ ํ•œ ๊ธ€์ž์”ฉ ์ž๋ฅธ ํ›„ ๊ฐ€์žฅ ์•ž์— ๊ธ€์ž๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ์ด์–ด ๋ถ™์ด๋Š” ๋ฐฉ์‹์ด๋‹ค.

<!-- HTML -->
<div>
  <span class="content"></span>
  <span class="blink" />
</div>
// JS
const $content = document.querySelector('.content');

function typewriter(target, sentence, speed = 200) {
  const split = sentence.split('');
  let text = '';
  let i = 0;

  const timer = setInterval(() => {
    if (i < split.length) {
      text += split[i++]; // ++๋Š” ํ›„์œ„ ์ฆ๊ฐ(์ฆ๊ฐ€ํ•˜๊ธฐ์ „ ๊ฐ’ ๋ฐ˜ํ™˜)
      target.textContent = text;
    } else {
      clearInterval(timer);
    }
  }, speed);
}

typewriter($content, 'hello world', 400);

 

๊นœ๋นก์ด๋Š” ์ปค์„œ ํšจ๊ณผ๋Š” | ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ–๋Š” <span> ํƒœ๊ทธ์—, step-end, step-start ๊ฐ™์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•œ๋‹ค. :after ์ˆ˜๋„ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  <span>|</span> ํ˜•ํƒœ๋กœ ์ž…๋ ฅํ•ด๋„ ๋œ๋‹ค.

/* CSS ๊นœ๋นก์ด๋Š” ์ปค์„œ ํšจ๊ณผ */
.blink::after {
  content: '|';
}

.blink {
  animation: blink 1s step-end infinite;
  font-weight: bold;
  margin-left: 1px;
}

@keyframes blink {
  50% {
    opacity: 0;
  }
}

 

React Hook์œผ๋กœ ๋งŒ๋“ค๊ธฐ


React์— ์ ์šฉํ•  ๋•Œ๋„ setInterval์„ ์ด์šฉํ•ด ์œ„์™€ ๋น„์Šทํ•œ ๋กœ์ง์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค.

 

  • Hook์ด ์ฒ˜์Œ ํ˜ธ์ถœ๋˜๋ฉด index ๋ฅผ 1์”ฉ ๋”ํ•˜๋Š” setInterval ๋‚ด๋ถ€ ํ•จ์ˆ˜๊ฐ€ sec์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.
  • ์ž…๋ ฅ๋ฐ›์€ ๋ฌธ์žฅ(ํ…์ŠคํŠธ)์˜ index๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์•ˆ๋˜๋ฏ€๋กœ content.length - 1 ๋ฏธ๋งŒ๊นŒ์ง€๋งŒ ์‹คํ–‰ํ•œ๋‹ค.
  • index ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด 2๋ฒˆ์งธ useEffect๊ฐ€ ์‹คํ–‰๋ผ์„œ ํ˜„์žฌ index์— ํ•ด๋‹นํ•˜๋Š” ๊ธ€์ž๋ฅผ ์ด์–ด ๋ถ™์ธ๋‹ค.

 

// hooks/useTypeWriter.js
export default function useTypeWriter({
  content,
  sec = 200,
  hasBlink = false,
}) {
  const [displayedContent, setDisplayedContent] = useState('');
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const animKey = setInterval(() => {
      setIndex((index) => {
        if (index < content.length - 1) {
          return index + 1;
        }
        clearInterval(animKey);
        return index;
      });
    }, sec);
    return () => clearInterval(animKey);
  }, []);

  useEffect(() => {
    setDisplayedContent(
      (displayedContent) => displayedContent + content[index],
    );
  }, [index]);

  return (
    <>
      {displayedContent}
      {hasBlink && <span className="blink" />}
    </>
  );
}

 

์ž‘์„ฑํ•œ Hook์€ ์•„๋ž˜์ฒ˜๋Ÿผ ์›ํ•˜๋Š” ์กฐ๊ฑด(๊นœ๋นก์ž„ ์ปค์„œ ์—ฌ๋ถ€, ์ž…๋ ฅ ์†๋„)์— ๋งž์ถฐ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

// Component.js
import useTypeWriter from '@/hooks/useTypeWriter';

export default function Component() {
  const typingText = useTypeWriter({
    content: 'Hello World',
    sec: 200,
    hasBlink: true,
  });

  return <p>{typewriterText}</p>;
}

 

์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ


๐Ÿ’ก ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์‹คํ–‰์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋Š” ์ดํ„ฐ๋Ÿฌ๋ธ”์ด๋ฉฐ, ํ•จ์ˆ˜ ์ž์ฒด์— ์ „๊ฐœ ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ†บ์•„๋ณด๊ธฐ

์ผ๋ฐ˜ ํ•จ์ˆ˜๋Š” 0~1๊ฐœ ๊ฐ’๋งŒ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ ๊ฐ’์„ ํ•„์š”์— ๋”ฐ๋ผ ํ•˜๋‚˜์”ฉ ๋ฐ˜ํ™˜(yield)ํ•  ์ˆ˜ ์žˆ๋‹ค. next()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด yield <value>๋ฌธ์„ ๋งŒ๋‚ ๋•Œ๊นŒ์ง€ ์‹คํ–‰ํ•˜๊ณ  value๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์•„๋ž˜ ์˜ˆ์‹œ์—์„  ํ•จ์ˆ˜ ์•ˆ์— while(true) ๋ฌธ์ด ์žˆ์œผ๋ฏ€๋กœ next() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค 'even' 'odd' ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ๋‹ค.

function* zebraGenerator() {
  while (true) {
    yield 'even';
    yield 'odd';
  }
}

const zebra = zebraGenerator(); // ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด ๋ฐ˜ํ™˜
zebra.next(); // {value: 'even', done: false}
zebra.next(); // {value: 'odd', done: false}

 

์ œ๋„ˆ๋ ˆ์ดํ„ฐ ๋ฌธ๋ฒ•์„ ํ™œ์šฉํ•ด ์ž…๋ ฅ๋ฐ›์€ ๋ฌธ์žฅ์„ ํ•œ ๊ธ€์ž์”ฉ ์ž๋ฅธ ๋’ค ๊ฐ€์žฅ ์•ž ๊ธ€์ž๋ถ€ํ„ฐ ์ด์–ด๋ถ™์ด๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ๋”์ด์ƒ ๋ฐ˜ํ™˜ํ•  value๊ฐ€ ์—†์œผ๋ฉด(sentenceAsCharArray ๋ฐฐ์—ด ์ˆœํšŒ๋ฅผ ๋งˆ์น˜๋ฉด) undefined๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ์ฒด๋‚ด done ์†์„ฑ์€ true๋กœ ๋ณ€ํ•œ๋‹ค.

function* textGenerator(sentence) {
  let text = '';

  const sentenceAsCharArray = sentence.split('');

  for (const letter of sentenceAsCharArray) {
    text += letter;
    yield text;
  }
}

const generator = textGenerator('HI');
generator.next(); // {value: 'H', done: false}
generator.next(); // {value: 'HI', done: false}
generator.next(); // {value: undefined, done: true}

 

React์— ์ ์šฉํ•˜๊ธฐ โญ๏ธ

์ž…๋ ฅ๋ฐ›์€ ๋ฌธ์žฅ์„ ํ•œ ๊ธ€์ž์”ฉ ์ž๋ฅธ ํ›„ ์ด์–ด๋ถ™์ด๋Š” ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋Š” utils ํŒŒ์ผ๋กœ ์˜ฎ๊ฒจ์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

// lib/utils.js
export function* textGenerator(sentence) {
  let text = '';

  const sentenceAsCharArray = sentence.split('');

  for (const letter of sentenceAsCharArray) {
    text += letter;
    yield text;
  }
}

 

๊ธฐ์กด ์ž‘์„ฑํ–ˆ๋˜ Hook์€ useEffect๋ฅผ 1๋ฒˆ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. setInterval ํ•จ์ˆ˜์˜ ๋”œ๋ ˆ์ด ๊ฐ„๊ฒฉ์— ๋”ฐ๋ผ generator.next() ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋ผ์„œ ํ•œ ๊ธ€์ž์”ฉ ์ด์–ด๋ถ™์ธ ํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค

// hooks/useTypeWriter.js
import { textGenerator } from '@/lib/utils';

export default function useTypeWriter({
  content,
  sec = 100,
  hasBlink = false,
}) {
  const [displayedContent, setDisplayedContent] = useState('');

  useEffect(() => {
    const generator = textGenerator(content);
    const interval = setInterval(() => {
      const { value, done } = generator.next();
      if (done) {
        clearInterval(interval);
      } else {
        setDisplayedContent(value);
      }
    }, sec);

    return () => clearInterval(interval);
  }, []);

  return (
    <>
      {displayedContent}
      {hasBlink && <span className="blink" />}
    </>
  );
}

 

์ง€์šฐ๊ณ  ๋‹ค์‹œ ์ž…๋ ฅํ•˜๋Š” ํšจ๊ณผ ๊ตฌํ˜„ โญ๏ธ


Step 1. Backspace ํšŸ์ˆ˜ ๊ณ„์‚ฐ

Hello World๋ฅผ ์ž…๋ ฅํ–ˆ๋‹ค๊ฐ€ World๋ฅผ ์ง€์šฐ๊ณ  Winter๋ฅผ ๋‹ค์‹œ ์ž…๋ ฅํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ W ์ด์ „๊นŒ์ง€ ๋ฐฑ์ŠคํŽ˜์ด์Šค๋ฅผ ๋ˆŒ๋Ÿฌ์„œ ํ•œ ๊ธ€์ž์”ฉ ์ง€์šฐ๊ณ  inter๋ฅผ ์ฐจ๋ก€๋Œ€๋กœ ์ž…๋ ฅํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์นœ๋‹ค.

Hello Worl   (Backspace)
Hello Wor    (Backspace)
Hello Wo     (Backspace)
Hello W      (Backspace) 
Hello Wi     (i ์ž…๋ ฅ)
Hello Win    (in ์ž…๋ ฅ)
Hello Wint   (int ์ž…๋ ฅ)
Hello Winte  (inte ์ž…๋ ฅ)
Hello Winter (inter ์ž…๋ ฅ)

 

d๋ถ€ํ„ฐ o(orld)๊นŒ์ง€ ์ง€์šฐ๊ธฐ ์œ„ํ•ด Backspace๋ฅผ 4๋ฒˆ ๋ˆŒ๋ €๋‹ค. Backspace๋ฅผ ๋ช‡ ๋ฒˆ ๋ˆŒ๋ €๋Š”์ง€ ์•Œ๊ธฐ ์œ„ํ•ด์„  ์›๋ž˜ ๋ฌธ์žฅ ๊ธธ์ด(length)์—์„œ, ์›๋ž˜ ๋ฌธ์žฅ/๋ฐ”๊ฟ€ ๋ฌธ์žฅ์˜ ๋งˆ์ง€๋ง‰ ๋™์ผ ๊ธ€์ž ์œ„์น˜(index + 1)๋ฅผ ๋นผ๋ฉด ๋œ๋‹ค.

Hello World
Hello Winter
------------
1234567 --> 7๋ฒˆ ์œ„์น˜(์ธ๋ฑ์Šค 6 + 1)๊นŒ์ง€ ๋™์ผ
------------
11(Hello World ๊ธธ์ด) - 7 = 4 -> Hello World์—์„œ Backspace 4๋ฒˆ
------------

 

์œ„ ๋‚ด์šฉ์€ ์•„๋ž˜์ฒ˜๋Ÿผ ํ•จ์ˆ˜๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ํ˜„์žฌ ๋ฐ˜๋ณต๋ฌธ์„ ์ˆœํšŒํ•˜๋Š” ์ธ๋ฑ์Šค์— ๋Œ€ํ•œ from, to ๊ธ€์ž๊ฐ€ ๊ฐ™๋‹ค๋ฉด charsIn... ๋ณ€์ˆ˜ ๊ฐ’์„ 1์”ฉ ๋”ํ•˜๊ณ , ๊ฐ™์ง€ ์•Š๋‹ค๋ฉด ๋ฐ˜๋ณต๋ฌธ์„ ๋ฉˆ์ถ”๊ณ  ๊ณ„์‚ฐํ•œ Backspace ํšŸ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

// lib/utils.js
export function calculateBackspaces(from, to) {
  let charsInCommonFromStart = 0; // from, to์˜ ๋งˆ์ง€๋ง‰ ๋™์ผ ๊ธ€์ž ์œ„์น˜(index + 1)

  for (let i = 0; i < from.length; i++) {
    const fromChar = from[i];
    const toChar = to[i];

    if (toChar === fromChar) {
      charsInCommonFromStart += 1; // Hello W ๊นŒ์ง€ ๋™์ผํ•˜๋ฏ€๋กœ 7
    } else {
      break;
    }
  }

  return from.length - charsInCommonFromStart; // 11 - 7 = 4
}

calculateBackspaces('Hello World', 'Hello Winter'); // 4

 

Step 2. Backspace ํšŸ์ˆ˜ ๋งŒํผ ๊ธ€์ž ์‚ญ์ œ

Backspace ํšŸ์ˆ˜๋ฅผ ์•Œ์•˜์œผ๋‹ˆ ์ด์ œ ๊ณ„์‚ฐํ•œ ํšŸ์ˆ˜๋งŒํผ ๊ธ€์ž๋ฅผ ์ง€์›Œ์ค€๋‹ค. ์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ์€ Backspace ํšŸ์ˆ˜๊ฐ€ 0์ด์–ด์„œ ์‚ญ์ œํ•  ๊ธ€์ž๊ฐ€ ์—†์œผ๋ฏ€๋กœ Backspace ํšŸ์ˆ˜ ๋งŒํผ ๊ธ€์ž๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฐ˜๋ณต๋ฌธ์€ ๊ทธ๋ƒฅ ๊ฑด๋„ˆ๋›ด๋‹ค.

// lib/utils.js
export function* textGenerator(sentences) {
  // sentences๋Š” ['Hello World', 'Hello Winter'] ๋ผ๊ณ  ๊ฐ€์ •(์›๋ž˜ ๋ฌธ์žฅ, ๋ฐ”๊ฟ€ ๋ฌธ์žฅ)
  let text = '';

  for (const sentence of sentences) {
    // (1) Backspace ํšŸ์ˆ˜ ๊ณ„์‚ฐ
    const backspaces = calculateBackspaces(text, sentence);
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 0
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 4

    // (2) ๊ณ„์‚ฐํ•œ Backspace ํšŸ์ˆ˜๋งŒํผ ๋’ค์—์„œ ๋ถ€ํ„ฐ ๊ธ€์ž ์‚ญ์ œ
    // ์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ ํ˜น์€ backspaces๊ฐ€ 0์ด๋ฉด ์•„๋ž˜ ๋ฐ˜๋ณต๋ฌธ์€ ๊ฑด๋„ˆ๋œ€
    for (let i = 0; i < backspaces; i++) {
      text = text.slice(0, -1);
      console.log(text);
      // 2๋ฒˆ์งธ ๋ฌธ์žฅ๋ถ€ํ„ฐ...
      // 'Hello Worl' -> 'Hello Wor' -> 'Hello Wo' -> 'Hello W'
    }
  }
}

 

Step 3. ๋ฐ”๊ฟ€ ๋ฌธ์žฅ ์ž…๋ ฅ

๐Ÿ’ก ์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ(sentence)์ผ ๋• text๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์—์„œ ‘H’ → ‘He’ → ... ํ•˜๋‚˜์”ฉ ์ด์–ด ๋ถ™์—ฌ์ง

 

Backspace ํšŸ์ˆ˜ ๋งŒํผ ๋ฌธ์žฅ์„ ์ง€์› ์œผ๋‹ˆ, ๋‹ค์‹œ ์ง€์šด ๋‚ด์šฉ์„ ์ฑ„์›Œ์•ผ ๋œ๋‹ค. ๋จผ์ € โžŠ๋น ์ง„ ๊ธ€์ž๋ฅผ ์ฐพ๊ณ , โž‹์ฐพ์•„๋‚ธ ๋น ์ง„ ๊ธ€์ž๋ฅผ ํ•œ ๊ธ€์ž์”ฉ ์ž๋ฅธ ๋’ค โžŒBackspaceํšŸ์ˆ˜๋งŒํผ ์‚ญ์ œํ•œ ๋ฌธ์žฅ์— ์ด์–ด๋ถ™์ด๋ฉด ๋œ๋‹ค.

// lib/utils.js
export function* textGenerator(sentences) {
  // sentences๋Š” ['Hello World', 'Hello Winter'] ๋ผ๊ณ  ๊ฐ€์ •(์›๋ž˜ ๋ฌธ์žฅ, ๋ฐ”๊ฟ€ ๋ฌธ์žฅ)
  let text = '';

  for (const sentence of sentences) {
    // (1) Backspace ํšŸ์ˆ˜ ๊ณ„์‚ฐ, (2) Backspace ํšŸ์ˆ˜ ๋งŒํผ ๊ธ€์ž ์‚ญ์ œํ•˜๋Š” ์ฝ”๋“œ ์ƒ๋žต...

    // (3) ๋ฐ”๊ฟ€ ๋ฌธ์žฅ ์ž…๋ ฅ
    const missingChars = sentence.slice(text.length);
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello World'.slice(0) -> 'Hello World'
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Winter'.slice(7) -> 'inter'
    const missingCharsArray = missingChars.split('');
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) ['H','e','l','l','o',' ','W','o','r','l','d']
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) ['i', 'n', 't', 'e', 'r']

    for (const missingChar of missingCharsArray) {
      text += missingChar;
      console.log(text);
      // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'H' -> 'He' -> 'Hel' -> 'Hell' -> 'Hello' -> ...
      // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Wi' -> 'Hello Win' -> 'Hello Wint' -> ...
    }
  }
}

 

Step 4. ๋‹ค์Œ ๋ฌธ์žฅ์œผ๋กœ ๋„˜์–ด๊ฐˆ ๋•Œ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€

'Hello World' ์ž…๋ ฅ์„ ๋งˆ์น˜๊ณ  ๋‹ค์Œ ๋ฌธ์žฅ 'Hello Winter' ๋กœ ๋ฐ”๋กœ ๋„˜์–ด๊ฐ€๋ฉด ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„  ์ฝ์„ ์‹œ๊ฐ„์ด ๋ถ€์กฑํ•˜๋‹ค. ๋”ฐ๋ผ์„œ ๋‘ ๋ฌธ์žฅ ์‚ฌ์ด์— delay ์‹œ๊ฐ„์ด ํ•„์š”ํ•˜๋‹ค. delay ์ˆซ์ž๋งŒํผ ๋™์ผํ•œ text๋ฅผ ๊ณ„์† ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ delay ์‹œ๊ฐ„์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.

// lib/utils.js
export function* textGenerator(sentences) {
  // sentences๋Š” ['Hello World', 'Hello Winter'] ๋ผ๊ณ  ๊ฐ€์ •(์›๋ž˜ ๋ฌธ์žฅ, ๋ฐ”๊ฟ€ ๋ฌธ์žฅ)
  let text = '';

  for (const sentence of sentences) {
    // (1) Backspace ํšŸ์ˆ˜ ๊ณ„์‚ฐ, (2) Backspace ํšŸ์ˆ˜ ๋งŒํผ ๊ธ€์ž ์‚ญ์ œํ•˜๋Š” ์ฝ”๋“œ ์ƒ๋žต...
    // (3) ๋ฐ”๊ฟ€ ๋ฌธ์žฅ ์ž…๋ ฅ ์ฝ”๋“œ ์ƒ๋žต...

    // (4) ๋‹ค์Œ ๋ฌธ์žฅ์œผ๋กœ ๋„˜์–ด๊ฐˆ๋•Œ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€
    const delay = 15;
    for (let i = 0; i < delay; i++) {
      console.log(text); // ํƒ€์ดํ•‘ ๋”œ๋ ˆ์ด ์†๋„๊ฐ€ 100ms๋ผ๋ฉด 100 * 15 = 1500(1.5์ดˆ)๊ฐ„ ๋”œ๋ ˆ์ด
      // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello World' 15๋ฒˆ ๋ Œ๋”
      // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Winter' 15๋ฒˆ ๋ Œ๋”
    }
  }
}

 

console.log → yield text ๋ณ€๊ฒฝ

Step 1~4์— ์ž…๋ ฅํ–ˆ๋˜ console.log(text)๋ฅผ yield text๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ๊ทธ๋Ÿผ โžŠyield ๋ฌธ์„ ๋งŒ๋‚  ๋•Œ๋งˆ๋‹ค text๊ฐ€ ํ™”๋ฉด์— ๋ Œ๋”๋˜๊ณ , โž‹useTypeWriter.js → useEffect ๋‚ด๋ถ€์— ์ž‘์„ฑํ•œ setInterval ํƒ€์ด๋จธ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด โž๋ฉˆ์ท„๋˜ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋ผ์„œ ์ด์ „ for ๋ฌธ์„ ์ด์–ด์„œ ์ˆœํšŒํ•œ๋‹ค.

๋”๋ณด๊ธฐ
export function calculateBackspaces(from, to) {
  // Hello World (from)
  // Hello Winter (to)
  // ------------
  // 1234567 --> 7๋ฒˆ ์œ„์น˜(์ธ๋ฑ์Šค 6 + 1)๊นŒ์ง€ ๋™์ผ
  // ------------
  // 11(Hello World ๊ธธ์ด) - 7 = 4 -> Hello World ์—์„œ Backspace 4๋ฒˆ
  // ------------

  let charsInCommonFromStart = 0;

  for (let i = 0; i < from.length; i++) {
    const fromChar = from[i];
    const toChar = to[i];

    if (toChar === fromChar) {
      charsInCommonFromStart += 1;
    } else {
      break;
    }
  }

  return from.length - charsInCommonFromStart;
}

export function* textGenerator(sentences) {
  // sentences ๋Š” ['Hello World', 'Hello Winter'] ๋ผ๊ณ  ๊ฐ€์ •(์›๋ž˜ ๋ฌธ์žฅ, ๋ฐ”๊ฟ€ ๋ฌธ์žฅ)
  let text = '';

  for (const sentence of sentences) {
    // (1) Backspace ํšŸ์ˆ˜ ๊ณ„์‚ฐ
    const backspaces = calculateBackspaces(text, sentence);
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 0
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 4

    // (2) ๊ณ„์‚ฐํ•œ Backspace ํšŸ์ˆ˜๋งŒํผ ๋’ค์—์„œ ๋ถ€ํ„ฐ ๊ธ€์ž ์‚ญ์ œ
    // ์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ ํ˜น์€ backspaces ๊ฐ€ 0์ด๋ฉด ์•„๋ž˜ ๋ฐ˜๋ณต๋ฌธ์€ ๊ฑด๋„ˆ๋œ€
    for (let i = 0; i < backspaces; i++) {
      text = text.slice(0, -1);
      yield text;
      // 2๋ฒˆ์งธ ๋ฌธ์žฅ๋ถ€ํ„ฐ...
      // 'Hello Worl' -> 'Hello Wor' -> 'Hello Wo' -> 'Hello W'
    }

    // (3) ๋ฐ”๊ฟ€ ๋ฌธ์žฅ ์ž…๋ ฅ
    const missingChars = sentence.slice(text.length);
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello World'.slice(0) -> 'Hello World'
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Winter'.slice(7) -> 'inter'
    const missingCharsArray = missingChars.split('');
    // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) ['H','e','l','l','o',' ','W','o','r','l','d']
    // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) ['i', 'n', 't', 'e', 'r']

    for (const missingChar of missingCharsArray) {
      text += missingChar;
      yield text;
      // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'H' -> 'He' -> 'Hel' -> 'Hell' -> 'Hello' -> ...
      // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Wi' -> 'Hello Win' -> 'Hello Wint' -> ...
    }

    // (4) ๋‹ค์Œ ๋ฌธ์žฅ์œผ๋กœ ๋„˜์–ด๊ฐˆ๋•Œ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€
    const delay = 15;
    for (let i = 0; i < delay; i++) {
      yield text; // ์„ค์ •ํ•œ ํƒ€์ดํ•‘ ์†๋„๊ฐ€ 100ms๋ผ๋ฉด 100 * 15 = 1500(1.5์ดˆ)๊ฐ„ ๋”œ๋ ˆ์ด
      // (์ฒซ๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello World' 15๋ฒˆ ๋ Œ๋”
      // (๋‘๋ฒˆ์งธ ๋ฌธ์žฅ) 'Hello Winter' 15๋ฒˆ ๋ Œ๋”
    }
  }
}
๋”๋ณด๊ธฐ
import { textGenerator } from '@/lib/utils';

export default function useTypeWriter({
  content,
  sec = 100,
  hasBlink = false,
}) {
  const [displayedContent, setDisplayedContent] = useState('');

  useEffect(() => {
    const generator = textGenerator(content);
    const interval = setInterval(() => {
      const { value, done } = generator.next();
      if (done) {
        clearInterval(interval);
      } else {
        setDisplayedContent(value);
      }
    }, sec);

    return () => clearInterval(interval);
  }, []);

  return (
    <>
      {displayedContent}
      {hasBlink && <span className="blink" />}
    </>
  );
}

 

1๊ฐœ ๋ฌธ์žฅ์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฌธ์žฅ์„ ๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์œผ๋ฏ€๋กœ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐฐ์—ด์— ์—ฌ๋Ÿฌ ๋ฌธ์žฅ์„ ๋„ฃ์–ด์„œ Hook์„ ํ˜ธ์ถœํ•œ๋‹ค.

// Component.js
import useTypeWriter from '@/hooks/useTypeWriter';

export default function Component() {

    const typingText = useTypeWriter({
    content: ['Hello World', 'Hello Winter'],
        sec: 200,
    hasBlink: true,
  });

    return <p>{typewriterText}</p>;
}

 

ํƒ€์ž๊ธฐ ํšจ๊ณผ ๋ฐ˜๋ณตํ•˜๊ธฐ

๊ธฐ๋ณธ์ ์œผ๋กœ ์ž…๋ ฅ๋ฐ›์€ ๋ชจ๋“  ๋ฌธ์žฅ์„ ์ˆœํšŒํ•˜๋ฉด ํƒ€์ž๊ธฐ ํšจ๊ณผ๋Š” ๋ฐ˜๋ณต์„ ๋ฉˆ์ถ˜๋‹ค(๊นœ๋นก์ด๋Š” ์ปค์„œ๋Š” ๊ณ„์† ๋ณด์ž„). ๋งŒ์•ฝ ํƒ€์ž๊ธฐ ํšจ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด(a ๋ฌธ์žฅ → b ๋ฌธ์žฅ → a ๋ฌธ์žฅ → ...) ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ while(true) ๋ฌธ์œผ๋กœ ๊ฐ์‹ผ ๋’ค ๋ฐ˜๋ณต ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋ฉˆ์ถœ์ง€, ๊ณ„์†ํ• ์ง€์— ๋Œ€ํ•œ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.

// lib/utils.js
export function* textGenerator(sentences, loop) {
  let text = '';

  while (true) {
    for (const sentence of sentences) {
      /* ์ƒ๋žต */
    }

    if (loop === false) {
      return;
    }
  }
}

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•