๐ค ์ด ๊ธ์ ์ฐ๊ฒ ๋ ์ด์
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>
);
};
๐ฏ ๋ง๋ฌด๋ฆฌ
์ด๋ฒ ์ฝ์ง์์ ๋ฐฐ์ด ๊ตํ๋ค
- useRef๋ก ๋ช ๋ นํ์ผ๋ก ์ ์ดํ๋ ค ํ์ง ๋ง๊ณ React์ ์ ์ธ์ ํน์ฑ์ ํ์ฉํ์.
- ๋ถํ์ํ ์์กด์ฑ์ด ์์์น ๋ชปํ ์ฌ์คํ์ ์ ๋ฐํ ์ ์๋ค(์ค๋ฌด์์ ์์ฃผ ๊ฒช์ ใ ). ํนํ ํจ์ํ props ์ฃผ์!
- 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 |