Travel Together 프로젝트

병용·2023년 9월 14일
0

Travel Of The Day

목록 보기
1/2

🛫 Travel Together Project

안녕하세요!
이번엔 여행동반자를 구인(?)하는 서비스를 만들어 왔습니다.
정말 오랜만에 개인으로 진행한 프로젝트인데요, 오늘 철야를 하고서 글을 쓰려니 아무말이나 막 나오네요. 양해 부탁드립니다. 허헣

이번 travel together 프로젝트는 혼자 여행 중에 중간중간 일행이 필요하거나 외로울 때, 동행을 구할 수 있도록 만든 프로젝트입니다.

기간은 9월 4일부터 14일까지 딱 열흘정도!
프론트는 react로, 백엔드는 django를 사용해서 구현했습니다.

컨셉이 컨셉이다 보니 채팅기능이 필요해서 django channels를 이용해서 구현했고 웹소켓은 프론트입장에서도 백입장에서도 처음이라 낯설었지만 그만큼 정상적으로 작동할 때 기쁨이 두배..!ㅎㅎ

프론트랑 백으로 나눠서 포스트 쓰려했는데, 막상 적으려다 보니 백엔드는 웹소켓 부분 제외하고는 늘 하던거다보니 프론트 위주로 작성하면서 백엔드쪽도 필요한 부분 언급하며 지나가도록 하겠습니다.

💤 언제까지 어깨춤을 추게할거야

🚌 게시글 목록

가장 먼저 보여드릴 페이지는 게시글 목록입니다.
동행이 필요한 사람이 게시글을 작성하고 관심있는 사람은 작성자에게 채팅을 걸거나 댓글을 다는 식으로 소통할 수 있게 해줬습니다.

이번 프로젝트 디자인은 tailwind css를 사용해서 반응형도 어느정도 볼만하게 만들어줬습니다. 반응형까지 했다!라고 말할 수 있는 게 드디어 하나 생겼네요:)

뒤에 배경은 제가 좋아하는 박보영배우님입니다🌟

게시글 목록에서는 해당 게시글의 작성자/작성일시/제목/댓글수 등을 보여주고 있습니다. 작성자는 클릭하면 바로 채팅방으로 이동할 수 있게 만들어줬습니다.

그러고보니 심리테스트 제외하면 로그인 없이도 사용자가 뭔가를 할 수 있는 서비스도 이번이 처음이네요 세상에..

갑자기 왜 이런 말을 하냐? 다음에 보여드릴게 로그인/회원가입 화면이기 때문이죠.

🎉 로그인 / 회원가입

먼저 로그인 화면입니다. 심플하니 괜찮죠? 요즘은 복작복작한 것보단 심플한게 예뻐보이드라구요.

이번엔 회원가입 화면인데요, 유효성검사하는 부분있어 gif로 가져와봤습니다.
아이디와 닉네임은 api로 중복검사 해주고 정상적으로 가입되면 로그인상태로 게시글 목록으로 보내주고 있습니다.

이번 프로젝트에서는 유저가 프로필이미지도 선택할 수 있게 했는데요. 6월에 진행했던 Bylog에서 해보려다가 못했었는데, 이번에 드디어 구현해봤습니다. 이번 프로젝트는 여러모로 처음 시도해보는 것들이 많은 것 같네요.

이미지는 사용자가 선택 시 미리보기를 지원해주고 회원가입 시 네이버클라우드의 object storage로 보내서 저장하고 url만 DB에 저장하고 있습니다. Bylog 때는 S3가 뭔지도 몰라서 그냥 포기했었는데 이제는 뚝딱뚝딱 바로 해버렸지 뭡니까 장하다 장해.

이미지는 input태그를 통해서 받고 있었는데, accept="image/*" 속성이 이미지 파일만 받을 수 있게 강제하는게 아니고 사용자지정파일로 하면 선택 가능하다고 해서 따로 jpg,jpeg,png만 받을 수 있게 유효성검사를 해줘야 했습니다.

위에 두 폼은 모두 react hook form을 사용해서 유효성검사를 따로 진행해줬습니다.

📑 게시글

게시글 상세보기 화면입니다. 댓글 작성하고 화면에는 안나왔지만, 대댓글까지 작성할 수 있게 만들어 줬습니다. 사실 처음 기획할 때는 진짜 채팅만 구현하자는 생각이였는데, 채팅 외에도 사용자 간 소통할 수단이 필요할 것 같아서 댓글/대댓글까지 구현했습니다.

게시글에서도 글쓴이에게 채팅할 수 있도록 만들어줬고 본인 글에서는 채팅하기버튼 대신 수정하기와 삭제하기 버튼이 랜더링 될 수 있도록 해줬습니다.

허허 단출한 저희 게시글 작성화면입니다. 뭐 에디터 같은 것도 없고 깔끔하니 좋네요..^^

게시글 수정하기도 있는데, 그냥 위에 작성하기 제목이랑 내용부분에 수정하는 게시글 제목이랑 내용 랜더링되게 해놓은거라 따로 스샷은 안올리겠습니다.

🙆‍♂️ 마이페이지

마이페이지입니다. 본인이 작성한 글하고 회원정보수정 화면으로 넘어갈 수 있는 버튼이 있네요.

역시 회원가입 폼에 정보만 넣어주고 바뀌면 안되는 것들 disabled만 설정해준 정도하 사진은 따로 준비안했습니다.

💬 채팅

이번 프로젝트의 메인인 채팅입니다..!

travel together에서는 단톡방은 없고 1대1 채팅만 구현되어 있습니다.

상대방 작성 시 입력중이라고 알려주는 문구가 출력되고 해당 채팅방에 들어와 있는지 유무에 따라 프로필이미지 뱃지의 색상을 변경해주고 있습니다.

입력중 표시는 메세지 input에 onChange 이벤트 발생 시 아래 코드로 전달해줬습니다.

  // 타이핑 안하는 중으로 상태 변경 후 ws에 typing false 송신
  function handleQuitTyping() {
    setTypingByMe(false);
    sendJsonMessage({ type: "typing", typing: false });
  }
  function handleTyping() {
    if (typingByMe === false) {
      // 타이핑 중 아니였으면 타이핑중으로 상태 변경 후 ws에 타이핑 메세지 송신
      setTypingByMe(true);
      sendJsonMessage({ type: "typing", typing: true });
      timeout.current = setTimeout(handleQuitTyping, 200); // 200 미리세컨드 뒤에 ws에 타이핑 종료 메세지 송신
    } else {
      // 이미 타이핑 중이였으면 handleQuitTyping 호출 200미리세컨드 지연
      clearTimeout(timeout.current);
      timeout.current = setTimeout(handleQuitTyping, 200); // 200 미리세컨드 뒤에 ws에 타이핑 종료 메세지 송신
    }
  }
  // 타이핑중인 사용자가 본인이면 입력중 표시 안나오게
  function updateTyping(typingData) {
    if (typingData.user !== username) {
      setTyping(typingData.typing);
    }
  }
  function handleChangeMessage(e) {
    setMessage(e.target.value);
    handleTyping();
  }

웹소켓으로 너무 많은 요청이 들어갈까봐 setTimeout을 사용하여 약간이지만 지연을 줬습니다.

온라인 표시는 사용자가 웹소켓 연결 시 해당 대화방 모델에 online 사용자로 추가해주고 프론트단에서는 상대방이 online 사용자에 해당하면 뱃지가 녹색이 되도록 처리해줬습니다.

채팅 수신했을 때 알림을 받기 위해 navbar와 채팅방 목록도 웹소켓으로 연결이 되어있습니다. 알림 받고 해당 대화방에 입장하면 read_message라는 타입으로 웹소켓에 보내서 그때까지 유저가 받은 메세지의 읽음 상태를 읽음으로 바꿔줬습니다.

consumers에서 작업하다보니 request.user를 사용 못 해서 채팅목록을 icontains=username으로 필터링해서 내려줬는데, 아이디가 시작이나 끝부분이 겹치는 사람들의 채팅까지 가져와져서 몹시 당황했던 기억이 나네요. 다행히도 1대1 채팅방이라 필터해줄 부분이 많지 않아서 금방 해결했답니다.

채팅방은 무한스크롤을 구현해서 페이지 당 50개씩의 메세지를 담아오는데, 스샷을 안찍어 왔네욤..허허

대신 코드입니다.

            className="flex flex-col-reverse"
            dataLength={messageHistory.length}
            next={loadMessages}
            inverse={true}
            hasMore={hasMoreMessages}
            loader={<Loading />}
            scrollableTarget="infinityScroll"

            <ul className="flex flex-col-reverse">
              {messageHistory.map((message, i) =>
                message?.from_user?.username === username ? (
                  <li key={i} className="flex flex-col">
                    <div className="flex flex-row-reverse">
                      <div className="border border-gray-200 bg-gray-100 w-1/2 md:w-1/3 rounded-md mb-2 px-2 text-right">
                        {message?.content}
                      </div>
                      <div className="text-xs text-gray-500 my-auto mx-1 mb-2">
                        <p>
                          {formatMessageTimestamp(message?.timestamp)?.date}
                        </p>
                        <p className="text-right">
                          {formatMessageTimestamp(message?.timestamp)?.hours}
                        </p>
                      </div>
                    </div>
                  </li>
                ) : (
                  <li key={i} className="flex flex-col">
                    <div className="font-semibold mb-1">
                      {message?.from_user?.nickname}
                    </div>
                    <div className="flex">
                      <div className="border border-blue-200 bg-blue-100 md:w-1/3 w-1/2 rounded-md mb-2 px-2">
                        {message?.content}
                      </div>
                      <div className="text-xs text-gray-500 my-auto mx-1 mb-2">
                        <p>
                          {formatMessageTimestamp(message?.timestamp)?.date}
                        </p>
                        <p>
                          {formatMessageTimestamp(message?.timestamp)?.hours}
                        </p>
                      </div>
                    </div>
                  </li>
                )
              )}
            </ul>
          </InfiniteScroll>

위에서 내가 보낸 메세지와 받은 메세지를 구분해서 각각 스타일을 해주고 있습니다.

💢 배포

이번 프로젝트에서는 gunicorn 대신 daphne라는 프로토콜 서버를 사용했다.
daphne는 http와 ws중 어떤 프로토콜로 처리할지 구분해준다고 한다.
django channels를 지원하기 위해 나온 친구라길래 세트로 사용해봤다.

아키텍쳐를 어떻게 그려야할지 감이 잘안와서 웹소켓 연결되어있는걸 양방향 화살표로 표시해 봤는데, 혹시 틀린 부분있으면 짚어주세요!

인터넷을 찾아보면 daphne를 쓰면 webserver를 안써도 된다 어쩐다 별소리가 다 있던데 나는 그냥 nginx를 사용하기로 했다.

ws를 nginx에서 어떻게 처리하나 봤더니 다들 location /ws/로 사용하길래 처음엔 ws프로토콜을 그냥 잡아주는건가 했는데, rest api url이 api/로 시작하는 것처럼 웹소켓 라우팅은 ws/로들 사용하는거였다.
프로토콜도 ws니까 그냥 그런건줄 알았지..

저 착각때문에 몇시간 허비하다가 씻고 온다고 집가는 길에 깨닫고 다시 컴퓨터 앞에 앉으니 금방 해결할 수 있었다.

혈 뚫리니 금세 ssl까지 마치고 이번 프로젝트 마무리!

🕒 회고

오랜만에 하는 개인 프로젝트, 분명 안될때 같이 짜증내주는 사람이 없어서 외롭긴 했지만 짜증나게 하는 사람도 없어서 편안..

처음 다뤄보는 웹소켓도 그렇고 클라우드 서비스 이용해서 프로필이미지 저장하는 것도 그렇고 팀을 이뤄서 했으면 혹시라도 구현 못해낼까봐 시도도 안해봤을 기능들을 마음껏 써볼 수 있었다.

처음 써보는 것들이 많아서 기간이 조금 타이트할까 생각했는데, 다행히도 생각한 수준에서 마무리할 수 있었다. 이틀정도 밤을 새긴 했지만..ㅋㅋ

짧은 기간 조그만한 프로젝트 진행한거다 보니까 쓸 말이 많이 없어서 이만 마치겠습니다. 처음 할때는 마냥 새롭고 어려웠는데, 막상 며칠 써보니 그러려니 해서 글로 적을 것도 별로 없네요. 프로젝트 진행하면서 새로운 것들 시도하다보면 자주 느끼지만 새로움은 금새 익숙함이 되는 것 같습니다. 감성적인 헛소리를 하는 것 보니 이제 진짜 쉬어야 할 것 같아요.

다음 포스트도 아마 개인 프로젝트 관련일 것 같습니다.

그럼 이만~하기전에

https://travel-together.shop/

이번 프로젝트 주소입니다. 궁금하신분들은 한번씩 들려주세요. 피드백 게시글로 남겨주시면 되는 선에서 반영하도록 노력해보겠십니더. :)

profile
횡설수설 정리노트

0개의 댓글