Open yellow-jam opened 1 month ago
react-typing-animation은 최근 업데이트가 2년 전이고, react-type-animation은 지난 주에 업데이트 했네요.
제 생각은 이렇습니다.. 라이브러리를 변경하고 구현하는 데 많은 시간이 걸리지 않으면 변경해도 괜찮을 것 같아요.
조금 늦었지만 챗봇 기능을 이루는 주요 컴포넌트를 설명하겠습니다. 백엔드와 연결하지 않은 상태입니다.
전체 동작: MessageForm 컴포넌트를 통해 메시지를 전송하면 ChatbotBox의 handleSendMessage에서 메시지 리스트(messages)를 업데이트합니다. (챗봇메시지의 id를 Date.now()
로 생성하여 사용 중) 메시지 리스트가 MessageList 컴포넌트를 통해 렌더링되고, 각각의 Item은 ChatMessage 컴포넌트로 선언되어 있습니다.
isUser
속성: 사용자가 보낸 채팅메시지면 true
, 챗봇이 응답한 메시지면 false
로 조건부 스타일링됩니다.채팅을 보내고 나서 스크롤바를 최하단으로 내리는 동작을 추가하는 과정에서 MessageList 컴포넌트를 forwardRef를 사용하여 선언했습니다.
함수형 컴포넌트는 인스턴스가 없어서 ref 속성을 사용할 수 없다. 커스텀 컴포넌트에서 ref를 통한 직접 제어가 필요할 때, React.forwardRef를 사용하면 부모 컴포넌트로부터 하위 컴포넌트로 ref를 전달할 수 있다. React Component를 forwardRef() 함수로 감싸주면, 컴포넌트 함수는 2번째 매개변수를 갖게 되는데 이를 통해 ref prop을 넘길 수 있다. forwardRef 참고자료
이것 때문에 코드가 복잡한 감이 있습니다... 커밋을 분리하지 않았더니 단계별 구현과정을 보기가 어렵네요. 반성합니다...
ChatbotBox에 선언된 것들
currentTypingId
: 타이핑 애니메이션을 재생할 채팅 메시지 (챗봇의 가장 마지막 메시지)isTyping
: 타이핑 애니메이션이 동작 중이면 truehandleEndTyping
: 타이핑이 끝났을 때 isTyping 속성을 false로 만들어주는 핸들러 (Typing 컴포넌트의 onFinishedTyping 리스너)(Message에 비하여) ChatMessage에 추가된 속성
onEndTyping
: 타이핑 애니메이션이 완료되면 onFinishedTyping 리스너에 의해 호출되는 handleEndTyping을 전달하기 위한 props입니다.currentTypingId
: ChatMessage에서 현재 메시지의 id와 currentTypingId를 비교하는 조건문이 있기 때문에 최상위 컴포넌트 ChatbotBox에서부터 아래로 아래로 전달되는 props입니다.Framer Motion을 사용한 애니메이션 동작에 대해 설명합니다.
조건부 렌더링을 적용하는 것만으로는 컴포넌트에 퇴장 애니메이션을 주기 어렵습니다. (unvisible 상태가 되면 바로 DOM에서 unmount되므로 퇴장 애니메이션이 보여지는 동안 delay될 필요가 있음) 이때 Framer Motion에서 지원하는 AnimatePresence를 통해 퇴장 애니메이션(exit animation)을 구현할 수 있습니다. element가 나타나거나 사라지는 상태를 감지(DOM을 비교)하여 애니메이션 효과를 발생시키는 wrapper 컴포넌트입니다.
기본적인 코드는 아래와 같습니다. 컴포넌트가 보여질 때 initial
→animate
애니메이션이 실행되며, 퇴장할 때 animate
→exit
애니메이션이 실행됩니다.
import { motion, AnimatePresence } from "framer-motion";
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
Framer Motion을 사용한 애니메이션 동작에 대해 설명합니다.
framer motion 컴포넌트의 기본형은 다음과 같습니다. (태그 안에 속성값을 직접 지정하는 방법)
<motion.div
initial={{ x: 0 }}
animate={{ x: -250 }}
/>
이때 motion 컴포넌트의 variants 속성을 사용하면 아래와 같이 작성할 수 있습니다. 애니메이션을 외부에 정의하여 재사용 가능해지며, 함수처럼 사용할 수도 있습니다. 변수에 따라 조건부로 애니메이션을 주고 싶을 때 custom 속성을 통해 전달합니다. 아래는 제 코드 예시입니다.
// 컴포넌트 내부의 코드
const buttonVariants = {
init: {
x: 0,
},
end: (isDrawerOpen: boolean) => ({
x: isDrawerOpen ? -250 : 0,
}),
};
return ( <ChatbotButton initial="init" animate="end" variants={buttonVariants} custom={isDrawerOpen} transition={{ type: 'tween' }} whileTap={{ scale: 0.9 }} onClick={() => { setIsDrawerOpen(!isDrawerOpen); }} /> )
### 축약된 형태
토글처럼 단순한 동작을 하는 경우 이런 식으로 축약해서 쓸 수도 있군요.
```tsx
// 예제
import { motion } from "framer-motion"
const variants = {
open: { opacity: 1, x: 0 },
closed: { opacity: 0, x: "-100%" },
}
export const MyComponent = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.nav
animate={isOpen ? "open" : "closed"}
variants={variants}
>
<Toggle onClick={() => setIsOpen(isOpen => !isOpen)} />
<Items />
</motion.nav>
)
}
제 코드를 리팩토링했습니다.
// 컴포넌트 내부의 코드
const buttonVariants = {
open: { opacity: 1, x: -250, zIndex: 1 },
closed: { opacity: 1, x: 0, zIndex: 1 },
};
return (
<ChatbotButton
animate={isDrawerOpen ? 'open' : 'closed'}
variants={buttonVariants}
transition={{ type: 'tween' }}
/>
)
import Typing from 'react-typing-animation';
...
<Typing
startDelay={30}
speed={50}
onFinishedTyping={() => id && onEndTyping(id)}
>
import { TypeAnimation } from 'react-type-animation';
...
<TypeAnimation
sequence={[
text,
() => {
id && onEndTyping(id);
},
]}
wrapper="span"
speed={50}
repeat={1}
cursor={false}
/>
⚙️ 기능 설명
변환 페이지의 챗봇 컴포넌트를 만듭니다.
✅ To-do
디자인
백엔드 연결
📑 참고 자료
Framer Motion
챗봇 아이콘(토글 버튼), 챗봇 drawer 열고 닫는 모션을 구현했습니다.