프로젝트/Tarss

[Tars] 회고록 7일차 - 채팅페이지 만들기

pizzaYami 2024. 2. 28.

처음으로 채팅페이지를 만들어 보았는데 많은 시행착오가 있었다.

 

만들어야 하는 채팅페이지 특징

기본적인 채팅페이지가 아니라 채팅형식으로 사용자의 데이터를 알아내기 위한 페이지이다.

 

기본적인 구조

상대방(편의상 dev라고 부르겠다)이 채팅을 통해서 설명을 진행한다.

그때 사용자는 {습관 선택}과 같은 버튼을 누르면 다른 페이지 또는 모달이 떠 데이터를 가져오는 형식이다.

 

예외적으로 버튼이 여러개 있는 경우도 있었다.

버튼 2개이상인 경우

데이터 구조

채팅에 대한 데이터를 저장을 위한 구조를 생각했다.

처음에 이 데이터를 어떻게 활용할 지 모르고 냅다 들이박아서 수정이 많았다.

다음에는 전체적인 그림을 먼저 그리자.

 

시도 1. 채팅 메시지만 배열에 넣기

무지성으로 이렇게 짜 보았는데

데이터를 가져올 때 숫자만 입력해서 어떠한 메시지를 가져오는지 명확하지 않았다.

const chattingData = [
    [
        `반가워요 ${userData.nickName}님! 저는 습관 형성 도우미 Tars에요 :)`,
        `어떤 습관을 함께 만들어 볼까요?`,
        `매일 해도 무리 없는 쉬운 것부터 시작하기를 추천해요 😊`,
    ],
    [
        `그렇군요!`,
        `이번엔 정체성을 정해 볼게요`,
        `${userData.habit}를(을) 통해서 ${userData.nickName}님은 어떤 사람이 되고 싶으세요?`,
    ],
    ...
];

 

 

시도 2. 채팅 메시지 객체에 넣기

어떠한 데이터를 가져오는지 명확했지만 map을 사용할 수 없었고 메시지와 버튼 Text를 따로 설정해야했다.

const chattingData = {
    firstMeet: [
        `반가워요 ${userData.nickName}님! 저는 습관 형성 도우미 Tars에요 :)`,
        `어떤 습관을 함께 만들어 볼까요?`,
        `매일 해도 무리 없는 쉬운 것부터 시작하기를 추천해요 😊`,
    ],
    ...
};

 

시도 3. 최종본

id를 통해서 객체를 구분하고 message와 replyBtnMessage를 구분해서 데이터를 만들었다.

replyBtnMessage는 답변 버튼이다.

위에서 이야기했듯이 답변 버튼이 여러개 일 경우도 있어서 배열에 넣어서 관리를 하였다.

export const chatData = [
    {
        id: "firstMeet",
        message: [
            `반가워요 ${userData.nickName}님! 저는 습관 형성 도우미 Tars에요 :)`,
            `어떤 습관을 함께 만들어 볼까요?`,
            `매일 해도 무리 없는 쉬운 것부터 시작하기를 추천해요 😊`,
        ],
        replyBtnMessage: ["습관 선택"],
    },
    {
        id: "habit",
        message: [
            `그렇군요!`,
            `이번엔 정체성을 정해 볼게요`,
            `${userData.habit}를(을) 통해서 ${userData.nickName}님은 어떤 사람이 되고 싶으세요?`,
        ],
        replyBtnMessage: ["정체성 선택"],
    },
    ...
    {
        id: "alert",
        message: [`약속을 기억하고 실천을 기록하실 수 있도록 하루 두 번, 알림을 보내드릴게요!`],
        replyBtnMessage: ["1차 알림 변경", "2차 알림 변경"],
    },
]

 

채팅 하나씩 올라오게하기

채팅을 할 때 하나씩 채팅이 올라오기 때문에 이에 대한 로직이 필요하였다.

setTimeout을 이용해서 1초마다 0부터 +1이 되도록 만들고

message를 slice를 이용해서 처음에는 slice(0, 1), slice(0, 2)...로 하나씩 올라가게 해서 잘라와 가져오게 할 것이다.

그런다음 잘라온 message를 map을 통해서 보여주면 끝!

 

// data
const message = [
    {
        id: "firstMeet",
        message: [
            `반가워요 피자냠냠님! 저는 습관 형성 도우미 Tars에요 :)`,
            `어떤 습관을 함께 만들어 볼까요?`,
            `매일 해도 무리 없는 쉬운 것부터 시작하기를 추천해요 😊`,
        ],
        replyBtnMessage: ["습관 선택"],
    },
];

const [messageIndex, setMessageIndex] = useState(0);

useEffect(() => {
    const timer = setTimeout(() => {
        if (message[0].message.length > messageIndex) { // 무한으로 숫자가 커지는 걸 방지
            setMessageIndex((prevIndex) => prevIndex + 1);
        }
    }, 1000);

    return () => clearInterval(timer); // timer 초기화
}, [messageIndex]);


return (
    <div>
        <ul>
            {message[0].message.slice(0, messageIndex + 1).map((i, index) => (
                <li key={index} className="message-item">
                    {i}    
                </li>
            ))}
        </ul>
    </div>
    );
};

 

버튼 누르면 다음 채팅 보이게 하기

 

시도 1. 합성컴포넌트로 만들고 display:none; 사용

<Chat.Message> 컴포넌트를 map으로 여러개 만들고 num을 통해서 display: none;을 제거하는 식으로 만들었다.

그랬더니 메시지가 한번에 떴다.

<Chat>
    {chatData.map((userData, index) => (
        <Chat.Message
            userData={userData}
            reply={userDataFrame.habit}
            progressProps={progressProps}
            num={index} // 답변버튼누르면 숫자가 늘어나고 display:none이 사라짐
        ></Chat.Message>
    ))}
</Chat>

 

 

 

시도 2. 합성컴포넌트제거하고 slice로 구현

컴포넌트 내부의 reply버튼을 누르면 progress가 +1되는데 이걸 이용해서 slice(0, progress +1)을 하여서 구현했더니 잘 되었다.

<div css={chattingContainer}>
    {chatData.slice(0, progress + 1).map((userData) => (
        <ChattingMessage
        key={userData.id}
        userData={userData}
        reply={userDataFrame.habit}
        progressProps={progressProps}
        ></ChattingMessage>
    ))}
</div>

 

자동으로 스크롤 다운

 

채팅을 하면 자동으로 스크롤 다운이 되어야하는데 그 기능을 어떻게 구현해야할 지 모르겠어서 검색을 해보았다.

https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView

// useRef는 DOM요소에 직접 접근하기 위해 사용된다.
// <HTMLDivElement>는 타입설정
const endOfMessagesRef = useRef<HTMLDivElement>(null);
   
// current는 실제 DOM 요소에 접근하기 위해 사용되는 속성
// 선택적 체이닝 연산자(?)를 사용하여 endOfMessagesRef.current이 null or undefined가 아닐경우에만 scrollIntoView 메서드를 호출한다.
// behavior: "smooth" 부드럽게 변경
const scrollToBottom = () => {
	endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" }); //
};

useEffect(() => {
	scrollToBottom();
}, [messageIndex]);



return (
    <div css={chattingStyle.container}>
        <div css={chattingStyle.profile}>
            <img src="" alt="profile" />
        </div>
            // 여기부분 DOM요소
        <div css={chattingStyle.chatWrap} ref={endOfMessagesRef}>
            <ul>
                {message.slice(0, messageIndex + 1).map((i, index) => (
                    <li key={index} className="message-item">
                	    {i}
                    </li>
                ))}
                {message.length === messageIndex && (
               	 <button onClick={handleReplyBtn}>{replyBtnMessage}</button>
                )}
            </ul>
            {isreply && <div css={replyStyle}>{reply}</div>}
        </div>
    </div>
    );
}

 

줄바꿈 안됨

백틱 (``)을 사용해서 줄 바꿈을 시도하였는데 안되서 chatGPT한테 물어봤다.

`그리고 그 약속은
1. 무엇을 할지 명확해야 하고
2. 하고 싶도록 매력적이며
3. 쉽게 할 수 있어야 하고
4. 하고 난 뒤 만족스러워야 합니다
`,

 

시도 1. <br>사용

<br>을 이용하라고 했지만 데이터를 가져와서 뿌리고 있기 때문에 <br>은 사용하지 못 하였다.

 

시도 2. white-space: pre-wrap;

css에 white-space: pre-wrap;를 추가하나깐

이상하게 나왔다.

console.log를 찍어보니 아래의 그림처럼 나와서 \n을 통해서 줄바꿈을 시도해보았다.

 

시도 3. \n의 사용

white-space: pre-wrap;를 사용하고 \n을 사용해서 줄바꿈을 해주니 내가 원하는대로 메시지가 나왔다.

`그리고 그 약속은
1. 무엇을 할지 명확해야 하고
2. 하고 싶도록 매력적이며
3. 쉽게 할 수 있어야 하고
4. 하고 난 뒤 만족스러워야 합니다
`,

// 이렇게 변경
`그리고 그 약속은 \n1. 무엇을 할지 명확해야 하고\n2. 하고 싶도록 매력적이며\n3. 쉽게 할 수 있어야 하고\n4. 하고 난 뒤 만족스러워야 합니다`,

 

 

 

댓글