Frontend/React

[React] React์—์„œ AI ํƒ€์ดํ•‘ ํšจ๊ณผ ๊ตฌํ˜„ํ•˜๊ธฐ | setInterval์˜ ํ•จ์ •๊ณผ ํ•ด๊ฒฐ๋ฒ•

2025. 9. 16. 17:49ยทFrontend/React

๐Ÿค” ์ด ๊ธ€์„ ์“ฐ๊ฒŒ ๋œ ์ด์œ 

AI ์ฑ„ํŒ… ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ChatGPT์ฒ˜๋Ÿผ ํ…์ŠคํŠธ๊ฐ€ ํ•œ ๊ธ€์ž์”ฉ ํƒ€์ดํ•‘๋˜๋Š” ํšจ๊ณผ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

"๊ทธ๋ƒฅ setInterval๋กœ ๊ธ€์ž ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•˜๋ฉด ๋˜๊ฒ ์ง€?"๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋Š”๋ฐ์š”... ์–ธ๋œป ๊ฐ„๋‹จํ•ด ๋ณด์ด๋Š” ๊ธฐ๋Šฅ์ด์—ˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” React์˜ ๋ Œ๋”๋ง ์‚ฌ์ดํด๊ณผ ๋น„๋™๊ธฐ ํƒ€์ด๋จธ๊ฐ€ ๋งŒ๋‚˜๋ฉด์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋“ค์ด ์†์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€์žฅ ๋‹นํ™ฉ์Šค๋Ÿฌ์› ๋˜ ๊ฒƒ์€ API ์‘๋‹ต์€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ›์•„์˜ค๊ณ , ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”๋ฐ ํ™”๋ฉด์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ๋Š” ๋ชจ๋“  ๊ฒƒ์ด ์ •์ƒ์ธ๋ฐ UI๋งŒ ๋ฉˆ์ถฐ์žˆ๋Š” ์ƒํ™ฉ... ์ •๋ง ๋ฉ˜๋ถ•.

์ด ๊ณผ์ •์—์„œ ๊ฒช์—ˆ๋˜ ์‚ฝ์งˆ๊ณผ ๊นจ๋‹ฌ์Œ์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ•˜๊ณ  ๊ณ„์‹  ๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿšซ ์ฒ˜์Œ ์ž‘์„ฑํ•œ ๋ฌธ์ œํˆฌ์„ฑ์ด ์ฝ”๋“œ

์ฒซ ๋ฒˆ์งธ ์‹œ๋„์—์„œ ๋งŒ๋“  useTypewriter ํ›…์€ ์ด๋žฌ์Šต๋‹ˆ๋‹ค:

// โŒ ๋ฌธ์ œ๊ฐ€ ์žˆ๋˜ ์ดˆ๊ธฐ ๋ฒ„์ „
export const useTypewriter = ({ 
  text, 
  speed = 30, 
  onComplete 
}: UseTypewriterProps) => {
  const [displayedText, setDisplayedText] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  const processedTextRef = useRef<string>('');

  useEffect(() => {
    // ๊ฐ™์€ ํ…์ŠคํŠธ๋ฉด ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ
    if (processedTextRef.current === text) {
      return;
    }

    if (!text) {
      setDisplayedText('');
      processedTextRef.current = '';
      return;
    }

    // ์ด์ „ ํƒ€์ด๋จธ ์ •๋ฆฌ
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }

    processedTextRef.current = text;
    setIsTyping(true);
    setDisplayedText('');

    let index = 0;
    timerRef.current = setInterval(() => {
      if (index < text.length) {
        setDisplayedText(text.substring(0, index + 1));
        index++;
      } else {
        setIsTyping(false);
        clearInterval(timerRef.current!);
        onComplete?.();
      }
    }, speed);

    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, [text, speed, onComplete]); // โš ๏ธ ์—ฌ๊ธฐ๊ฐ€ ๋ฌธ์ œ์˜ ํ•ต์‹ฌ!

  return { displayedText, isTyping };
};

๊ฒ‰์œผ๋กœ๋Š” ๊ทธ๋Ÿด๋“ฏํ•ด ๋ณด์ด์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ์˜€์Šต๋‹ˆ๋‹ค.


๐Ÿ” ๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ์„๊นŒ?

1. ์˜์กด์„ฑ ๋ฐฐ์—ด์˜ ํ•จ์ •

}, [text, speed, onComplete]); // โŒ onComplete์ด ๋ฌธ์ œ

onComplete ํ•จ์ˆ˜๊ฐ€ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์–ด useEffect๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์žฌ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. React์—์„œ๋Š” ํ•จ์ˆ˜๋„ ๊ฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์ฐธ์กฐ๊ฐ’์„ ๊ฐ€์ง€๊ฒŒ ๋˜์ฃ .

// ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด...
<TypewriterComponent 
  text={aiResponse}
  onComplete={() => console.log('์™„๋ฃŒ!')} // ๋งค๋ฒˆ ์ƒˆ ํ•จ์ˆ˜!
/>

2. setInterval๊ณผ React ์ƒํƒœ์˜ ๋ฏธ๋ฌ˜ํ•œ ์ถฉ๋Œ

let index = 0;
timerRef.current = setInterval(() => {
  setDisplayedText(text.substring(0, index + 1));
  index++; // ํด๋กœ์ € ๋‚ด์—์„œ ๋ณ€์ˆ˜ ๋ณ€๊ฒฝ
}, speed);

setInterval ๋‚ด๋ถ€์˜ ํด๋กœ์ €์—์„œ index ๋ณ€์ˆ˜๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋ฉด์„œ React์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ์™€ ํƒ€์ด๋ฐ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๋น ๋ฅธ ์†๋„๋กœ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ผ์–ด๋‚  ๋•Œ ๋ Œ๋”๋ง์ด ๋ฐ€๋ฆฌ๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

3. ๋ณต์žกํ•œ Ref ๊ด€๋ฆฌ

const timerRef = useRef<NodeJS.Timeout | null>(null);
const processedTextRef = useRef<string>('');

๋‘ ๊ฐœ์˜ ref๋กœ ํƒ€์ด๋จธ์™€ ์ค‘๋ณต ์‹คํ–‰์„ ๊ด€๋ฆฌํ•˜๋‹ค ๋ณด๋‹ˆ ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๊ณ  ์˜ˆ์ธกํ•˜๊ธฐ ์–ด๋ ค์›Œ์กŒ์Šต๋‹ˆ๋‹ค. React์˜ ์„ ์–ธ์  ํŠน์„ฑ๊ณผ ๋งž์ง€ ์•Š๋Š” ๋ช…๋ นํ˜• ์ฝ”๋“œ๊ฐ€ ๋˜์–ด๋ฒ„๋ฆฐ ๊ฒƒ์ด์ฃ .


โœ… ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ ์ƒˆ๋กœ์šด ์ ‘๊ทผ๋ฒ•

๊ณ ๋ฏผ ๋์— ์™„์ „ํžˆ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์žฌ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค:

// โœ… ๊น”๋”ํ•˜๊ฒŒ ํ•ด๊ฒฐ๋œ ๋ฒ„์ „
export const useTypewriter = ({ 
  text, 
  speed = 30, 
  onComplete 
}: UseTypewriterProps) => {
  const [displayedText, setDisplayedText] = useState('');
  const [isTyping, setIsTyping] = useState(false);

  useEffect(() => {
    if (!text) {
      setDisplayedText('');
      setIsTyping(false);
      return;
    }

    setDisplayedText('');
    setIsTyping(true);

    let currentIndex = 0;

    const typeCharacter = () => {
      if (currentIndex < text.length) {
        // ํ˜„์žฌ ๊ธ€์ž๊นŒ์ง€ ํ‘œ์‹œ
        setDisplayedText(text.substring(0, currentIndex + 1));
        currentIndex++;
        // ๋‹ค์Œ ๊ธ€์ž๋ฅผ ์œ„ํ•œ ์žฌ๊ท€ ํ˜ธ์ถœ
        setTimeout(typeCharacter, speed);
      } else {
        // ํƒ€์ดํ•‘ ์™„๋ฃŒ
        setIsTyping(false);
        onComplete?.();
      }
    };

    // ์ฒซ ๊ธ€์ž๋ถ€ํ„ฐ ์‹œ์ž‘
    setTimeout(typeCharacter, speed);
    
  }, [text, speed]); // onComplete ์ œ๊ฑฐ!

  return { displayedText, isTyping };
};

์ด๊ฒŒ ๋์ž…๋‹ˆ๋‹ค! useRef๋„ ์—†๊ณ , ๋ณต์žกํ•œ ์กฐ๊ฑด๋ฌธ๋„ ์—†๊ณ , ํ›จ์”ฌ ๊ฐ„๋‹จํ•˜์ฃ ?


๐ŸŽฏ ํ•ต์‹ฌ ๊ฐœ์„  ํฌ์ธํŠธ

1. setTimeout ์žฌ๊ท€ ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜

const typeCharacter = () => {
  if (currentIndex < text.length) {
    setDisplayedText(text.substring(0, currentIndex + 1));
    currentIndex++;
    setTimeout(typeCharacter, speed); // ๐ŸŽฏ ์žฌ๊ท€ ํ˜ธ์ถœ
  } else {
    setIsTyping(false);
    onComplete?.();
  }
};

์™œ setTimeout ์žฌ๊ท€๊ฐ€ ๋” ์ข‹์„๊นŒ?

setInterval์˜ ๋ฌธ์ œ์ :

  • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ interval์„ throttlingํ•จ
  • React ์ƒํƒœ ์—…๋ฐ์ดํŠธ์™€ ํƒ€์ด๋ฐ์ด ์•ˆ ๋งž์Œ
  • ์ •ํ™•ํ•œ ๊ฐ„๊ฒฉ ๋ณด์žฅ์ด ์–ด๋ ค์›€

setTimeout ์žฌ๊ท€์˜ ์žฅ์ :

  • ๊ฐ ์‹คํ–‰์ด ์™„์ „ํžˆ ๋…๋ฆฝ์ 
  • ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ ํ›„ ๋‹ค์Œ ํƒ€์ด๋จธ ์„ค์ •
  • ๋” ์ •ํ™•ํ•˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ํƒ€์ด๋ฐ

2. ์˜์กด์„ฑ ๋ฐฐ์—ด ์ตœ์ ํ™”

}, [text, speed]); // onComplete ์ œ๊ฑฐ๋กœ ๋ถˆํ•„์š”ํ•œ ์žฌ์‹คํ–‰ ๋ฐฉ์ง€

onComplete ์ฝœ๋ฐฑ์„ ์˜์กด์„ฑ์—์„œ ์ œ๊ฑฐํ•ด์„œ ๋ถˆํ•„์š”ํ•œ useEffect ์žฌ์‹คํ–‰์„ ๋ง‰์•˜์Šต๋‹ˆ๋‹ค. ์–ด์ฐจํ”ผ ํƒ€์ดํ•‘์ด ์™„๋ฃŒ๋  ๋•Œ๋งŒ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ ์ตœ์‹  ๊ฐ’์ด ์ž๋™์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

3. useRef ์™„์ „ ์ œ๊ฑฐ

๋” ์ด์ƒ ref๋กœ ํƒ€์ด๋จธ๋‚˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. React์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด lifecycle์— ๋งก๊ธฐ๋‹ˆ ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์กŒ์Šต๋‹ˆ๋‹ค.


๐Ÿ“Š ์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ

// ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
const AIResponse = ({ content }) => {
  const { displayedText, isTyping } = useTypewriter({
    text: content,
    speed: 50, // 50ms ๊ฐ„๊ฒฉ
    onComplete: () => {
      console.log('AI ์‘๋‹ต ์™„๋ฃŒ!');
      // ์™„๋ฃŒ ํ›„ ์ถ”๊ฐ€ ๋™์ž‘ (์˜ˆ: ์Œ์„ฑ ์ฝ๊ธฐ, ๋‹ค์Œ ์งˆ๋ฌธ ํ™œ์„ฑํ™” ๋“ฑ)
    }
  });

  return (
    <div className="ai-message">
      <div className="content">
        {displayedText}
        {isTyping && <span className="cursor animate-pulse">|</span>}
      </div>
      {!isTyping && (
        <div className="actions">
          <button>์ข‹์•„์š”</button>
          <button>๋ณต์‚ฌ</button>
        </div>
      )}
    </div>
  );
};

// ์ฑ„ํŒ… ๋Œ€ํ™”์—์„œ ์‚ฌ์šฉ
const ChatConversation = ({ messages }) => {
  return (
    <div className="chat-container">
      {messages.map((message, index) => (
        <div key={index} className={`message ${message.role}`}>
          {message.role === 'assistant' ? (
            <AIResponse content={message.content} />
          ) : (
            <div>{message.content}</div>
          )}
        </div>
      ))}
    </div>
  );
};

 


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ์‚ฝ์งˆ์—์„œ ๋ฐฐ์šด ๊ตํ›ˆ๋“ค

  1. useRef๋กœ ๋ช…๋ นํ˜•์œผ๋กœ ์ œ์–ดํ•˜๋ ค ํ•˜์ง€ ๋ง๊ณ  React์˜ ์„ ์–ธ์  ํŠน์„ฑ์„ ํ™œ์šฉํ•˜์ž.
  2. ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ์ด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์žฌ์‹คํ–‰์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ๋‹ค(์‹ค๋ฌด์—์„œ ์ž์ฃผ ๊ฒช์Œ ใ…Ž). ํŠนํžˆ ํ•จ์ˆ˜ํ˜• props ์ฃผ์˜!
  3. setInterval๋ณด๋‹ค setTimeout ์žฌ๊ท€๊ฐ€ React ์ƒํƒœ ์—…๋ฐ์ดํŠธ์™€ ๋” ์ž˜ ์–ด์šธ๋ฆฝ๋‹ˆ๋‹ค.
์ €์ž‘์žํ‘œ์‹œ ๋น„์˜๋ฆฌ ๋ณ€๊ฒฝ๊ธˆ์ง€ (์ƒˆ์ฐฝ์—ด๋ฆผ)

'Frontend > React' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[RN] URL ํŒŒ๋ผ๋ฏธํ„ฐ vs ์Šคํ† ์–ด ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…: React Native ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฆฌํŒฉํ† ๋ง ์™„๋ฒฝ ๊ฐ€์ด๋“œ  (0) 2025.09.27
[React] React Hook "useEffect" is called in function ์—๋Ÿฌ ํ•ด๊ฒฐ  (0) 2025.03.03
[React] .env ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ๋ฒ• (Vite / CRA ํ™˜๊ฒฝ)  (0) 2025.02.27
[React] Props, Event, State  (2) 2024.12.19
[React] ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ๊ณ ์ฐฐ  (3) 2024.12.18
'Frontend/React' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [RN] URL ํŒŒ๋ผ๋ฏธํ„ฐ vs ์Šคํ† ์–ด ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…: React Native ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฆฌํŒฉํ† ๋ง ์™„๋ฒฝ ๊ฐ€์ด๋“œ
  • [React] React Hook "useEffect" is called in function ์—๋Ÿฌ ํ•ด๊ฒฐ
  • [React] .env ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ๋ฒ• (Vite / CRA ํ™˜๊ฒฝ)
  • [React] Props, Event, State
sol_git
sol_git
Full-Stack์„ ๊ฟˆ๊พธ๋Š” Junior Developer๐Ÿ’–
  • sol_git
    ์†”๊นƒํ•œ Dev
    sol_git
  • ๊ธ€์“ฐ๊ธฐ ๊ด€๋ฆฌ์ž
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (44)
      • Frontend (15)
        • Javascript (1)
        • React (11)
        • Vue (1)
        • Svelte (1)
      • Style Sheet (0)
        • Sass (0)
      • Backend (1)
        • Java (3)
        • Python (1)
        • Spring Boot (0)
      • AI (1)
        • LLM (0)
        • Gen AI (0)
      • DevOps (16)
        • Git (16)
        • Kubernetes (0)
      • Cloud (0)
        • AWS (0)
      • DBMS (2)
        • MySQL (1)
        • PostgreSQL (1)
      • IDE & Tools (3)
        • IntelliJ (1)
        • VS Code (1)
        • Tool (1)
      • OS (2)
        • Mac (2)
      • Project ์ผ๊ธฐ (0)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ๋ฐฉ๋ช…๋ก
  • ๋งํฌ

    • Github
  • ์ธ๊ธฐ ๊ธ€

  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.3
sol_git
[React] React์—์„œ AI ํƒ€์ดํ•‘ ํšจ๊ณผ ๊ตฌํ˜„ํ•˜๊ธฐ | setInterval์˜ ํ•จ์ •๊ณผ ํ•ด๊ฒฐ๋ฒ•
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”