Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4주차] 신동현 미션 제출합니다. #21

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

dhshin98
Copy link

@dhshin98 dhshin98 commented Nov 3, 2023

배포링크

https://ceos-messenger.vercel.app/

디자인_피그마링크

image

<신경썼던 부분>

  1. 반응형으로 짜기 시작해서 페이지별로 모바일, PC등 모든 화면에서 예쁘게 나오게 하려고 노력했습니다.

특히 친구 목록 페이지에서 어떻게 보여줄지 고민했는데, 모바일 & PC& 패드의 경우의 수 세가지로 나눠서 비율에 따라 친구를 보여줬습니다.

image
  1. 4주차 구현을 위해 추가적으로 메시지 전송 시간 정보를 추가하고, 기존 채팅방에 마지막 메시지에 아이콘 오류를 수정하였습니다.

  2. 사용자의 마지막 메시지를 저장하고 관리하는 법:
    chatlist페이지에 채팅별 마지막 메시지를 가져오고, 업데이트 해야해서 이 부분에 대한 고민을 했습니다.
    ⇒ 마지막 메시지를 가져올때는 TS의 타입중 Record<K,T>를 사용해서 Record<string, Message | null>로 마지막 메시지의 시간, 메시지 content를 찾았습니다.
    ⇒ 예를 들어, 새 메시지가 도착하거나, 사용자가 특정 메시지를 읽었을 때 unread의 상태를 업데이트하여 해당 변화를 화면에 반영했습니다.
    ⇒ 이 마지막 메시지의 시간을 기준으로 가장 최근에 소통한 사람 리스트를 sorting 했습니다.

작동과정

1. profile 페이지

제일 먼저 뜨는 페이지로,

image

첫번째 아이템을 누르면 제 깃허브로 연동이 되고, 마지막 아이템을 누르면 채팅방 목록페이지 (2. chatlist 페이지) 으로 넘어갑니다.

2. chatlist 페이지

image 왼쪽이 답장 전, 오른쪽이 답장 보낸 후 화면입니다.

chatlist 페이지는

  1. 현재 채팅기록이 있는 채팅방의 목록만 필터링하여 보여줍니다.
  2. 시간순으로 최신 순서로 정렬했습니다.
  3. 시간은 오늘인 경우는 시간이 뜨고, 전날짜는 어제, 그 전은 __일전 과 같이 뜹니다.
  4. 상대방이 보낸 경우, 앞에 파란색 점으로 알림이 뜨고, 내가 답장을 보낸 후에는 사라집니다.
  5. SearchTab 에서는 사람 이름을 검색하게 해서 필터링된 아이템만 보여줍니다.

각각의 채팅방을 누르면 chat 페이지 (지난과제에서 구현했었던) 으로 이동이 됩니다.

3. Friends 페이지

2페이지의 채팅방 목록의 상단에 있는 +Friends 요 버튼을 누르면 친구 목록 페이지로 넘어갑니다.

image 기능으로는 검색기능이 있습니다.

채팅방에는 나를 포함한 유저 목록이 뜹니다.

  1. 상태 메시지를 설정한 경우 버블로 위에 상태메시지를 뜨게 했습니다.
  2. 각 친구 아이콘을 누르면 chat 페이지로 이동합니다.
  3. SearchTab에서는 검색한 user 이름이 뜨게 합니다.
  4. 새로운 메시지를 시작하고 싶으면 여기서 사용자를 눌러서 채팅을 시작하면 됩니다.

고민했던 지점:

여러 채팅방의 데이터를 어떻게 관리할까 고민하다가 ChatData라는 인터페이스를 사용하였습니다. , 각 사용자 ID에 해당하는 메시지 배열을 관리하는 인터페이스로, 기존에 chat 페이지에서 사용한 Message를 상대방 userId: Message로 매핑한 구조를 만들어 local storage 에 저장하면서 사용했습니다.
interface ChatData {
[key: string]: Message[];
}
image

[Key Questions]

image
  1. 메세지 버블 정렬
    마지막 메세지 버블들이 왼쪽으로 더 치우쳐 있습니다. 모든 버블들이 같은 레이아웃으로 정렬되도록 수정 부탁드립니다.

    ⇒ 마지막 메시지 버블이 치우친 문제도 margin 조정을 통하여 해결하였습니다. (아래는 수정한 사진 입니다!)

image
  • QA 반영한 커밋(or 브랜치) 링크 (커밋 분리 필수!!!)
  1. [QA1] input 변경 커밋 링크 :
    dhshin98@de442f5
  2. [QA2] 메시지 버블 커밋 링크:
    dhshin98@fe2f338
  • Routing
    어떤 네트워크 내에서 통신 데이터를 보낼 경로를 선택하는 일련의 과정, 경로를 정해주는 행위 자체와 그런 과정들을 다 포함하여 일컫는 말입니다.

  • SPA
    SPA (Single Page Application)란 한 개(Single)의 Page로 구성된 Application이다. 현재의 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 애플리케이션이나 웹사이트를 말한다. React는 SPA인데, 일반 웹 사이트처럼 URL에 따른 페이지 이동을 할 수 있게 해주는 기능이 React Router이다.

  • 상태관리
    상태관리는 useState와 useEffect 훅을 사용하여 상태 관리를 하였습니다. 기본적인 상태관리 함수를 사용하려고 했습니다. Recoil, Redux등의 전역상태관리를 해보고 싶었지만, 이번 과제에서는 진행하지 못한 것 또한 아쉬운 점인 것 같습니다.

느낀점

라우팅까지 구현하다보니까 재밌게 과제했습니다. 다만 코드의 재사용성에 대한 고민은 계속 해봐야할것 같습니다. 어찌저찌 구현을 한 뒤에도 코드가 안예쁜거 같아서 계속 더 고민하게 되는 것같습니다.
추가로, 디자이너 분께 필요하거나 궁금한 부분은 카톡으로 연락드렸고, 바로 답변주셔서 빠르게 소통이 가능했습니다.
추가 구현을 하게 된다면 상태메시지의 수정이 기능 같은 것이 있으면 좋겠다는 생각도 들었습니다.
계속 작업은 했는데 QA 수정을 하고 난 뒤로는 커밋을 틈틈히 못날려서 기능단위로 커밋을 못 날린게 아쉽습니다. 다음 과제부터는 꼭 바로바로 커밋을 날리겠습니다!!

Copy link

@oooppq oooppq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시험도 보고 과제도 하느라 너무너무 수고하셨어요~~ 디자인 사항을 충실히 적용하려고 많이 노력하신 것 같아요!! 디렉토리도 페이지랑 컴포넌트를 나눠 깔끔한 것 같습니다 너무너무 수고하셨어요~~

Comment on lines +21 to +22
"jsx": "react-jsx"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"jsx": "react-jsx"
},
"jsx": "react-jsx",
"baseUrl": "src",
},

위와 같이 baseUrl 속성을 지정해주면, 컴포넌트나 메소드를 임포트할 때 src 이후의 절대경로로 불러올 수 있어요! 예를 들어,

import bigIcon from "../../assets/images/BigIcon.svg";

이렇게 임포트한 부분을

import bigIcon from "assets/images/BigIcon.svg";

이런식으로 바꿀 수 있어요!

기능상 문제는 없지만 만약, 디렉토리의 뎁스가 깊어지면 상대경로로 참조했을 때 가독성이 다소 떨어질 수 있어서,
절대경로로 임포트해봐도 좋을 것 같아요~~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이런 방법이..!! 앞으로는 이런식으로 해봐야겠네요~! 감사합니다 😊

Comment on lines +30 to +36
function getFilteredUsers(users: User[], searchTerm: string) {
if (!searchTerm) return users;
const filteredUsers = users.filter((user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return filteredUsers;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같은(비슷한) 기능을 하는 함수는 따로 빼서 다른 파일에 정의해주는 것도 좋을 것 같아요!
최대한 추상화해서 다양한 상황에서 두루 쓰일 수 있게끔 하면 구현에도 편리하고 관심사분리도 잘 이뤄질 수
있을 것이라고 생각해요~~

filter를 사용해서 검색된 유저를 가져오고, lower case로 검사하는 디테일 좋은 것 같습니다!!

) as ChatData;

// 모든 사용자의 마지막 메시지와 시간 가져오기
const lastMessage: Record<string, Message | null> = {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record 타입은 한 번도 사용해본 적 없는데 적절한 상황에서 잘 사용하신 것 같아요 저도 써보겠습니당~~

Comment on lines +56 to +61
return {
...user,
lastMessageTimestamp: lastChat
? new Date(lastChat.timestamp) // 마지막 메시지가 있으면 해당 시간을 가져옴
: new Date(0), // 마지막 timestamp 없으면 기본 시간으로 예외처리
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastMessage를 따로 관리할 필요 없이 요 return 값 안에 각각의 lastMessage를 포함했어도 괜찮았을 것 같아요!

Comment on lines +71 to +82
const lastMessageUnread: Record<string, Message | null> = {};
// 모든 사용자의 마지막 메시지에 대한 unread 상태 업데이트 :
// 마지막 sender가 내(신동현)가 아닌 경우 unread 로 처리
userData.users.forEach((user) => {
const lastMsg = lastMessage[user.id];
if (lastMsg) {
lastMessageUnread[user.id] = {
...lastMsg,
unread: lastMsg.sender !== "신동현",
};
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

읽음처리에 대한 디테일 좋은 것 같습니다~~

Comment on lines +90 to +95
const savedChats = JSON.parse(
localStorage.getItem("chatMessages") || JSON.stringify(chatData)
) as ChatData;

savedChats[chatId] = updatedMessages;
localStorage.setItem("chatMessages", JSON.stringify(savedChats));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

채팅방별로 데이터를 관리하는 것이라면 하나의 key value 쌍으로 localStorage에 저장하지 않고 각각 분리해서 저장했어도 괜찮았을 것 같아요! key를 chatMessages_${상대유저이름}와 같은 방식으로 지정하면 어렵지 않게 구현할 수 있을 것 같습니당
지금은 chatMessages안의 모든 data를 불러오고 새롭게 저장하는 과정이 있기 때문에 불필요한 데이터 로딩이 클 것 같아요!

물~론 저는 모든 메시지들을 한 번에 관리해서 지금 동현님 한 것 처럼 한 번에 불러오도록 하긴 했지만요ㅋㅋ.. 근데 채팅방 별로 관리하면 충분히 분리해서 저장할 수 있을 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터를 어떻게 관리할까 고민했었는데, 말씀하신대로 분리해서 관리하면 좋았었겠네요!!!

Comment on lines +134 to +146
const messageContainers = messages.map((message: Message, index) => {
const isCurrentUser = message.sender === nowUser.name;

return (
<Message
key={message.id}
sender={message.sender}
content={message.content}
nowUser={nowUser.name}
showIcon={message.showIcon && !isCurrentUser}
/>
);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showIcon을 판단할 때, 위처럼 따로 로직을 만들 수도 있지만, map 안에서 index를 통해 다음 message의 sender와 비교하여 sender가 바뀌는 시점에 showIcon을 true로 설정해주는 방법을 사용해도 좋을 것 같아요!

Suggested change
const messageContainers = messages.map((message: Message, index) => {
const isCurrentUser = message.sender === nowUser.name;
return (
<Message
key={message.id}
sender={message.sender}
content={message.content}
nowUser={nowUser.name}
showIcon={message.showIcon && !isCurrentUser}
/>
);
});
const messageContainers = messages.map((message: Message, index) => {
const isCurrentUser = message.sender === nowUser.name;
return (
<Message
key={message.id}
sender={message.sender}
content={message.content}
nowUser={nowUser.name}
showIcon={message.sender !== messages[index + 1].sender && !isCurrentUser}
/>
);
});

이런 식으로요!

Comment on lines +21 to +22
<Search src={searchIcon} />
<Record src={recordIcon} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 컴포넌트 네이밍을 할 때 용도를 조금 더 드러내서 네이밍해도 괜찮을 것 같아요! Search나 Record만 이게 아이콘의 기능을 하는지, Search input의 기능을 하는지 버튼의 기능을하는지 알기 쉽지 않을 수 있거든요!

Comment on lines +24 to +54
function formatTimestamp(isoString?: string) {
if (!isoString) return "No last message";

const date = new Date(isoString);
date.setHours(0, 0, 0, 0);

const now = new Date();
now.setHours(0, 0, 0, 0);

const diffTime = now.getTime() - date.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

// 날짜 차이에 따른 문자열 반환
//오늘은 시간 정보, 전날은 "어제", 그 전날은 '~일전'으로 반환해줌
if (diffDays === 0) {
const date = new Date(isoString);
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12;
hours = hours ? hours : 12;
const strHours = hours < 10 ? `0${hours}` : `${hours}`;
const strMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
return `${strHours}:${strMinutes} ${ampm}`;
} else if (diffDays === 1) {
return "어제";
} else {
return `${diffDays}일 전`;
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요론 함수도 별도의 파일에 정의하는게 가독성에 좋을 것 같습니다! 재사용할 가능성도 있구요!

Copy link

@kyuhho kyuhho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

긴 시험기간동안 고생 많으셨습니다!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소하지만 lowerCamelCase를 지켜서 일관성 있게 네이밍하면 좋을거 같아요!

Comment on lines +30 to +31
:hover {
cursor: pointer;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지 전체적으로 curosr가 포인터로 되어있는 거 같아요. 필요한 요소에만 적용된다면 더 좋을거 같습니다!

Comment on lines +135 to +146
@media (max-width: 850px) {
// 화면 너비가 850px 이하인 경우 (ipad 환경)
width: 100%;
max-width: 20%; // item이 한줄에 5개씩 오도록
}

@media (max-width: 480px) {
// 화면 너비가 480px 이하인 경우 (모바일 환경)
width: 100%;
max-width: 33%; // item이 한줄에 3개씩 오도록
margin-right: 0;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반응형으로 처리해준 디테일이 좋아요!

Comment on lines +66 to +68
{isInputFocused ? (
//handleSendClick 작동?? click 작동
<Send onClick={handleSendClick} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에서 onClick 보다 onBlur가 우선순위를 가지면서 클릭되기 전에 <Send> 가 사라져 클릭으로 메세지가 안 보내지는 이슈가 있는거 같아요. 저 같은 경우에는 onMouseDown으로 해결했습니다! 한번 읽어보시면 좋을거 같아요 ㅎㅎ

Suggested change
{isInputFocused ? (
//handleSendClick 작동?? click 작동
<Send onClick={handleSendClick} />
{isInputFocused ? (
//handleSendClick 작동?? click 작동
<Send onMouseDown={handleSendClick} />

참고자료

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 그렇군요!!! 사실 계속 안돼서 구현을 못했던 부분인데, 참고해서 수정해야겠네요~!!! 알려주셔서 감사합니다~~🙇‍♀️ㅎㅎㅎ

Comment on lines +74 to +82
userData.users.forEach((user) => {
const lastMsg = lastMessage[user.id];
if (lastMsg) {
lastMessageUnread[user.id] = {
...lastMsg,
unread: lastMsg.sender !== "신동현",
};
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

읽음 처리 정보를 처리해준게 정말 좋아요! 새로 배워갑니다 ㅎㅎ 개인적으로 읽음 처리가 채팅방 조회 시 사라지는 플로우가 추가되면 더욱 좋을거 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

�오 그게 더 좋은 플로우일 것 같네요~!! 시간이 되면 추가해보겠습니다 :) 감사합니당

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants