1. 세팅
- 디자인 레퍼런스: WaveeAI - AI Chat Dashboard
- shadcn-ui 설치 -> shadcn-ui docs 참고 (저는 현재 vite를 사용중이므로 해당 내용 참고하였습니다)
2. 필요한 컴포넌트 구성
디자인 레퍼런스를 보면 크게 왼쪽 사이드바와 메인으로 구성되어있습니다. 저는 여기에 추가적인 무언가를 할 예정이므로 네비바까지 추가하게되었습니다.
또한, 제가 구현하려는것은 결국 채팅앱이기때문에 채팅을 입력하는 컴포넌트와 채팅을 렌더링하는 컴포넌트가 필요했습니다.
3. 코드 분리
기존코드
const SendMessage = () => {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState<string[]>([]);
const [socket, setSocket] = useState<Socket | null>(null);
const [activity, setActivity] = useState("");
const [isTyping, setIsTyping] = useState(false);
const typingTimeoutRef = useRef<number | null>(null);
useEffect(() => {
const newSocket = io("http://localhost:5000");
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}, []);
useEffect(() => {
if (socket) {
socket.on("activity", (name) => {
setActivity(`${name}님이 작성중입니다,,,`);
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setActivity("");
}, 1000);
});
socket.on("message", (data) => {
setMessages((prev) => [...prev, data]);
});
}
return () => {
if (socket) {
socket.off("message");
}
};
}, [socket]);
const keyDownHandler = () => {
if (socket && !isTyping) {
socket.emit("activity", socket.id.substring(0, 5));
setIsTyping(true);
}
};
const keyUpHandler = () => {
setIsTyping(false);
};
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (message !== "" && socket) {
socket.emit("message", message);
setMessage("");
}
};
return (
<div>
<form onSubmit={submitHandler}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={keyDownHandler}
onKeyUp={keyUpHandler}
/>
<button type="submit">Send</button>
</form>
<ul>
{messages.map((msg, idx) => (
<li key={idx}>{msg}</li>
))}
</ul>
<p>{activity}</p>
<h2>{socket?.id?.substring(0, 5)}</h2>
</div>
);
};
를 아래와 같이 분리해줍니다
HomePage.tsx
해당 컴포넌트에서는 socket.io 연결을 통해 서버와의 통신을 담당합니다
const HomePage = () => {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<string[]>([]);
const [activity, setActivity] = useState("");
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const newSocket = io("http://localhost:5000");
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}, []);
useEffect(() => {
if (socket) {
socket.on("activity", (name) => {
setActivity(`${name}님이 작성중입니다,,,`);
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setActivity("");
}, 1000);
});
socket.on("message", (data) => {
setMessages((prev) => [...prev, data]);
});
}
return () => {
if (socket) {
socket.off("message");
}
};
}, [socket]);
return (
<div className="flex flex-col gap-8 h-full">
<div className="p-4 -mt-10 bg-white/90 h-[90%] rounded-2xl">
<Message messages={messages} activity={activity} />
</div>
<ChatForm socket={socket} />
</div>
);
};
Message.tsx
해당 컴포넌트는 메시지를 렌더링하는 역할을 맡습니다
type MessageProps = {
messages: string[];
activity: string;
};
const Message = ({ messages, activity }: MessageProps) => {
return (
<div className="text-black">
<ul className="flex flex-col gap-2">
{messages.length === 0 ? (
<div>메시지가 없습니다,,,</div>
) : (
messages.map((message, idx) => (
<div key={idx}>
<Badge variant="secondary" className="py-1.5 px-2 rounded-sm">
{message}
</Badge>
</div>
))
)}
</ul>
<p className="text-sm text-gray-400 mt-4">{activity}</p>
</div>
);
};
ChatForm.tsx
해당 컴포넌트는 입력을 컨트롤하는 역할을 맡습니다
type ChatFormProps = {
socket: Socket | null;
};
const ChatForm = ({ socket }: ChatFormProps) => {
const [isTyping, setIsTyping] = useState(false);
const form = useForm<z.infer<typeof messageSchema>>({
defaultValues: {
inputMessage: "",
},
});
const keyDownHandler = () => {
if (socket && !isTyping) {
socket.emit("activity", socket.id.substring(0, 5));
setIsTyping(true);
}
};
const keyUpHandler = () => {
setIsTyping(false);
};
const submitHandler = (values: z.infer<typeof messageSchema>) => {
if (socket) {
socket.emit("message", values.inputMessage);
form.reset();
}
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(submitHandler)}
className="py-2 rounded-lg relative"
>
<FormField
control={form.control}
name="inputMessage"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="text"
{...field}
onKeyDown={keyDownHandler}
onKeyUp={keyUpHandler}
className="text-black font-semibold"
/>
</FormControl>
</FormItem>
)}
/>
<button type="submit" className="absolute top-4 right-4">
<Send className="w-6 h-6 text-black" />
</button>
</form>
</Form>
);
};
## 참고
저는 form을 컨트롤할때 "zod"와 "react-hook-form"을 사용하였습니다.
4. 결과
아직 디자인을 수정해야할게 많지만, 초기 디자인으로는 괜찮은거같습니다.
'OLD > chat-app' 카테고리의 다른 글
[ChatApp] Socket.io를 활용하여 간단한 채팅 어플 만들어보기 - 03. Clerk을 이용한 인증 구현 (+ prisma) (0) | 2023.10.15 |
---|---|
[ChatApp] Socket.io를 활용하여 간단한 채팅 어플 만들어보기 - 01. 연결 및 간단한 소통 (2) | 2023.10.14 |