코딩/사이드 프로젝트

[react 블로그 만들기 #2] contact chat ui 구현

dduu뚜 2024. 8. 6. 16:47

채팅에 쓰인 가사는 라이즈 - 원키스 

만들고 보니까 채팅바를 왜 하단에 고정 안 시켜두었는지 모르겠지만. 추후 수정하는 걸로... 

저 채팅 ui 는 contact 페이지다. 

 

예전 팀 과제 할 때 팀원 한 분이 소켓통신으로 채팅 기능을 구현하셨는데 그때는 개발이라는 게 정말 정말 UI 좀 깔짝 거리는 게 다였어서 그냥 대단하다. 하고 박수만 쳤던 게 끝이었다. 지금도 관심은 가는 데 스트레스 받고 힘들 것 같아서 그것 까진 무리무리. 대신 언젠가는 채팅 형식의 UI를 구성해보는 것도 나쁘진 않을 것 같다는 생각을 했었다. 쓸 데도 없고 그땐 실력도 뭣도 없어서 그냥 속으로만 생각했지만. 지금도 지피티 도움을 받아서 고민 시간을 반의 반으로 줄이면서 하고는 있지만, 예전 꼬꼬마 시절보단 훨씬 쉽고 빠른 시간 내에 이해할 수 있다는 것에 의의를 두고 있다. 성장한 거지 뭐 별 거 있어...

 

하여튼 CURD 기능은 없고, 일단은 UI만 만들어 놨다. 그러니까 옷만 입혀놨다는 거다. 이 옷에 어울리는 액세서리와 화장품을 추가하려면 좀 걸릴 듯. 기획 설계할 때 아 이왕 하는 거 백단도 구성해보자 -> 그러면 8월 안에 완성 못 할 것 같음 -> 백단 버리고 firebase로 가자 -> 사소한 거 하나하나 디자인 하고 싶어 -> 그러면 8월 안에 못 끝냄 -> 더 덜어내자  < 지금 8월 6일 기준으로 여기까지 왔다. 기능 붙이는 걸 나중에 하고 다른 페이지 디자인부터 하는 게 좋을 것 같음. 모든 페이지에 다 firestore를 쓸 거기 때문에 일단은.... 이것도 언제 바뀔 지 모른다. 

 

이 프로젝트에서 사용중인 폰트들은 나중에 따로 정리해서 올리겠음. 

 

const ContactPage = () => {

    return (
        <>
            <Header/>
            <ChatMsg/>

        </>
    )
}

ContactPage.tsx 

 

 

ChatMsg 다음과 같이 나뉘어져 있다. 

 

- <ChatTitle> 채팅방 입장 초기 문구

- <SpeechBubble>  말풍선

-<ChatInput> 채팅창 

 

원래 ChatInput을 따로 분리하지 않고 chatMsg에 놔뒀다가 오늘 글 쓰면서 급하게 분리. 설계 대충하면 이렇게 된다...  

const ChatTitle = () => {
    const date = new Date();
    const formatter = new Intl.DateTimeFormat('ko-KR', {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    });
    const today = formatter.format(date);


    return (

        <div className="flex flex-col justify-center items-center mt-10">
            <motion.div
                initial={{y: 50, opacity: 0}} // 초기 상태: 화면 아래에서 위로 이동 및 투명
                animate={{y: show ? 0 : 50, opacity: show ? 1 : 0}} // 애니메이션 상태: 위로 이동 및 보임
                transition={{duration: 1, ease: "easeOut"}} // 애니메이션 지속 시간과 easing
                exit={{y: 50, opacity: 0}} // exit 애니메이션: 다시 아래로 이동 및 투명
            >
                <div className="w-72 h-14 shadow-lg rounded-2xl bg-gray-50 flex items-center justify-center">
                    <span className=" text-base font-normal" style={{fontFamily: 'CoreDream'}}>📆 {today}</span>
                </div>
            </motion.div>

 

애니메이션 라이브러리로

import { motion} from "framer-motion";

 

framer-motion 사용하고 있는데, 라이브러리를 최대한 걷어내자 주의지만 애니메이션 css 를 잘 모르기도 하고 지금 하나하나 필요한 거 적용해보면서 커스텀 해보고 익히는 중이라 좀 더 효율적으로 하고자 사용 중이다. 꽤 기능도 많고 편한 것 같음. 한국어로 설명된 게 없어서 간단한 기능 예시로는 지티피로 뽑아내고 있다. 

 

 

css 말풍선 간단하게 만들어주는 사이트가 있어서 기본적인 건 여기서 가지고 왔다. 세상 편함... 

 

https://projects.verou.me/bubbly/

 

Bubbly — CSS speech bubbles made easy

 

projects.verou.me

 

const SpeechBubble = ({name, phone, email, content} : textInfo) => {

    const [show, setShow] = useState(false);
    useEffect(() => {
        setShow(true);
    }, []);

    return (
        <motion.div
            initial={{y: 50, opacity: 0}} // 초기 상태: 화면 아래에서 위로 이동 및 투명
            animate={{y: show ? 0 : 50, opacity: show ? 1 : 0}} // 애니메이션 상태: 위로 이동 및 보임
            transition={{duration: 1, ease: "easeOut"}} // 애니메이션 지속 시간과 easing
            exit={{y: 50, opacity: 0}} // exit 애니메이션: 다시 아래로 이동 및 투명
        >
            <div css={bubbleCss}>
                <div className="chatBubble shadow-lg" style={{fontFamily: 'CoreDream'}}>{content}</div>
            </div>
        </motion.div>

    )

}

SppechBubble.tsx 

 

css는 따로 분리 하지 않고 여기 넣어주었다. 

 

css 수정하면서 selection도 좀 바꾸었다. 원래는 저 말풍선 배경색이 selection 되는 건데, 저 말풍선에 그대로 사용하면 색이 겹치기 때문에 진한 노란색으로 바꿔보았다. 글귀 내용은 <몰락의 에티카> 신형철. 제일 좋아하는 평론가....책 또 언제 내주시나요. 그의 모든 글 중 역시 압도적으로 좋아하는 서문. 아마 나말고도 좋아하는 사람들 많은 걸로 알고 있다. 

 

.chatBubble {
    width: 400px;
    min-height: 50px;
    height: auto;

 

최소 높이를 설정하고 height를 auto로 해서 안에 있는 content 크기에 따라 높이가 자동적으로 조절 되게 했다. 패딩은 아래 정도로 줌. 

padding: 20px;

 

const ChatInput = ({getBubbles}) => {
    const [show, setShow] = useState(false);
    const [content, setContent] = useState<string>('')

    const addBubble = () => {
        if(content.trim()) {
           getBubbles( {name: "w뚜뚜", phone: "0101231234", email: "dududu@naver.com", content });

            setContent('')

        }

    }

    const onEnter = (e) => {
        if(e.key === 'Enter') {
            e.preventDefault();
            addBubble();
        }
    }

    const changeContent = (e) => {
        setContent(e.target.value);
    }

ChatInput.tsx

 

채팅창에서 enter를 누르거나 bubble 버튼을 클릭하면 ChatMsg에 있는 getBubbles 함수가 호출되면서 관련 내용을 가지고 가 출력한다. 

 

원래 상위 -> 하위로 값을 넘기는 작업만 해봤는데 이번엔 거꾸로 작업하는 거라 초반에 좀 헤맸지만 .... 뭔가 더 효율적인 방법이 있는 것 같은데 나중에 강의 들으면서 보충해야겠다. 지금은 로직 자체가 복잡한 게 아니라서 이 정도면 될 듯 하다. 

 

interface getChatData {
    name : string;
    phone : string;
    email : string;
    content : string;
}

const ChatMsg = () => {

    const [bubbles, setBubbles] = useState<getChatData[]>([]);
    
    const newBubbles = (newData: getChatData) => {
        setBubbles([...bubbles, newData]);
    }

    return (
        <>
            <div className="w-full border-t-2 border-black">
                <div css={msgCss}>
                    <ChatTitle/>
                    {bubbles.map((item, index) => (
                        <SpeechBubble key={index} name={item.name} phone={item.phone} email={item.email} content={item.content}/>
                    ))}
                </div>
                    <ChatInput getBubbles={newBubbles}/>

            </div>
        </>
    )
}

ChatMsg.tsx

 

아무튼 컴포넌트를 쪼갠 덕분에 여기선 꽤나 간단해졌다. 지저분하게 나열됐던 객체 내용들을 interface로 하나로 묶어 넣어주고, setBubbles에 스프레드 연산자를 사용해서 기존 버블 내용을 복사한 뒤 받아온 객체를 추가해준다. 

 

그러면 위와 같이 말풍선이 계속 생기면서 내가 추가한 버블 내용이 잘 나오는 걸 알 수 있음. 이름과 전화번호, 이메일까지 넣어서 만드려고 했는데 어떤 UI가 좋을지 안 떠오르기도 하고 내가 이걸 배포하게 되면 저 세 가지는 넘나 사적인 개인 정보인데 이걸 그대로 노출하는 건 아니지않나 싶기도 해서 앞으로 어떻게 활용할 지는 좀 생각해 봐야 할 것 같음.