[4주차] 김홍엽 과제 제출합니다.#14
Conversation
chaeyoungwon
left a comment
There was a problem hiding this comment.
과제하시느라 수고 많으셨습니다!
전반적으로 서비스에 필요한 요소들을 잘 파악하고
피그마에서 디자이너분과 적극적으로 소통해주신 것 같아 인상 깊었습니당 👏
There was a problem hiding this comment.
채팅 리스트 페이지에서는 실제 채팅 앱처럼,
채팅방 목록이 가장 최근에 전송된 메시지를 기준으로 최신순 정렬되도록 갱신하는 로직을 작성해보셔도 좋을 것 같습니다 !!
| className={`w-8 h-8 ${ | ||
| isActive ? "text-primary" : "text-content-inactive" | ||
| }`} |
There was a problem hiding this comment.
앞으로 조건부 스타일링을 자주 사용하시게 될 텐데, 이때 clsx 라이브러리나 cn 유틸 함수를 활용하시는 것을 추천드려봅니당
| {tabs.map((tab) => { | ||
| const isActive = activeTab === tab.key; | ||
| return ( | ||
| <button |
There was a problem hiding this comment.
버튼 요소에는 가급적 cursor-pointer 속성도 함께 추가해주시면 좋을 것 같습니다!
There was a problem hiding this comment.
특정 컴포넌트들에 memo를 적용해주셨는데, 어떤 기준으로 적용 여부를 판단하셨는지 궁금하네용 ~~
| useEffect(() => { | ||
| if (chatRoomId !== null) { | ||
| setActiveChatRoom(chatRoomId); | ||
| } | ||
| return () => { | ||
| setActiveChatRoom(null); | ||
| }; | ||
| }, [chatRoomId, setActiveChatRoom]); |
There was a problem hiding this comment.
| { "id": 4, "name": "쩝", "participantIds": [0, 4] }, | ||
| { | ||
| "id": 5, | ||
| "name": "이름이엄청나게길어질경우에는원채영바보", |
| <div className="flex items-center justify-between px-5 py-3 h-12"> | ||
| <span | ||
| className="text-h1-logo" | ||
| style={{ color: "var(--color-primary)" }} |
There was a problem hiding this comment.
text-primary를 className에 바로 적용해주시면 될 것 같네용
| {chatRooms.length === 0 && ( | ||
| <span className="mt-25 self-center text-body2-r text-content-hint"> | ||
| 현재 참여 중인 대화방이 없습니다. | ||
| </span> | ||
| )} |
There was a problem hiding this comment.
피그마에 별도로 요청해주신 점 인상 깊네요!
실제 서비스에서도 다양한 상황이 발생할 수 있기 때문에, 앞으로도 이런 엣지 케이스까지 고려해 개발해주시면 좋을 것 같습니다 ~~
| import IconAddFriend from "@/assets/icons/icon_add_friend.svg?react"; | ||
| import IconAddStory from "@/assets/icons/icon_add_story.svg?react"; | ||
| import IconFacebook from "@/assets/icons/icon_facebook.svg?react"; | ||
| import IconNotification from "@/assets/icons/icon_notification.svg?react"; | ||
| import IconUserChat from "@/assets/icons/icon_user_chat.svg?react"; | ||
| import IconUserMe from "@/assets/icons/icon_user_me.svg?react"; | ||
| import IconUserStory from "@/assets/icons/icon_user_story.svg?react"; | ||
| import IconUp from "@/assets/icons/icon_up.svg?react"; | ||
| import IconDown from "@/assets/icons/icon_down.svg?react"; |
There was a problem hiding this comment.
아이콘 import문이 다소 길어지고 있어, assets/icons/index.ts에서 한 번에 모아 export한 뒤 가져오는 방식도 고려해보시면 좋을 것 같습니다!
| const tabs: { | ||
| key: TabKey; | ||
| label: string; | ||
| Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>; | ||
| path: string; | ||
| }[] = [ | ||
| { | ||
| key: "home", | ||
| label: "홈", | ||
| Icon: IconHome, | ||
| path: "/", | ||
| }, | ||
| { | ||
| key: "chat", | ||
| label: "채팅", | ||
| Icon: IconChat, | ||
| path: "/chat", | ||
| }, | ||
| { | ||
| key: "menu", | ||
| label: "메뉴", | ||
| Icon: IconMenu, | ||
| path: "", | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
경로는 상수로 분리해서 관리해보셔도 좋을 것 같아요!
오타를 줄일 수 있고, 경로 변경 시에도 한 곳에서만 수정하면 된다는 장점이 있습니당
minseo0614
left a comment
There was a problem hiding this comment.
전체적으로 디자인 토큰화, 컴포넌트 분리, 메시지 그룹핑 같은 핵심 영역에서 완성도가 높아서 코드 읽으면서 배운 점이 많았습니다!
| const filteredRooms = useMemo(() => { | ||
| const query = searchQuery.trim().toLowerCase(); | ||
| if (!query) return chatRooms; | ||
|
|
||
| return chatRooms.filter((room) => { | ||
| if (room.name.toLowerCase().includes(query)) return true; | ||
| return getMessagesByChatRoomId(room.id).some((msg) => | ||
| msg.content.toLowerCase().includes(query), | ||
| ); | ||
| }); | ||
| }, [chatRooms, searchQuery, getMessagesByChatRoomId]); |
There was a problem hiding this comment.
채팅방 이름뿐 아니라 메시지 내용까지 검색 대상에 포함된 점이 사용자 입장에서 실용적인 것 같아요!
| getUnreadCount: (chatRoomId, currentUserId) => { | ||
| const lastReadId = get().lastReadMessageId[chatRoomId] ?? 0; | ||
| return get().messages.filter( | ||
| (m) => | ||
| m.chatRoomId === chatRoomId && | ||
| m.senderId !== currentUserId && | ||
| m.id > lastReadId | ||
| ).length; |
There was a problem hiding this comment.
unread를 별도 배열로 관리하지 않고 lastReadMessageId 하나만 저장한 뒤, 그 기준으로 카운트를 계산하는 방식이 좋았습니다. senderId !== currentUserId 조건으로 내가 보낸 메시지를 자연스럽게 제외한 점도 실제 메신저 동작과 잘 맞는 것 같습니다.
| const handleKeyDown = (e: React.KeyboardEvent) => { | ||
| if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { | ||
| e.preventDefault(); | ||
| handleSubmit(); | ||
| } |
There was a problem hiding this comment.
MessageInput에서 Enter 전송을 처리할 때 !e.nativeEvent.isComposing 조건까지 둔 점이 좋은 것 같아요! 한글 조합 중 Enter로 메시지가 잘려 전송되는 버그를 막아주는 세심한 처리라고 생각했습니다.

배포 링크
🔗 https://ceos-week3-react-messenger-23rd-nine.vercel.app/
디자이너와 협업하며 전달받은 자료
피그마 링크
🔗 https://www.figma.com/design/es0RHym54Js6oyP8kgJDxb/CEOS-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%EA%B9%80%EC%A0%95%EC%9B%90?node-id=0-1&t=arMirpAfb4nTqRT9-1
커뮤니케이션 과정
Figma와 카카오톡을 통해 디자이너와 양방향 커뮤니케이션 및 QA 진행했습니다.
Review Questions
1. React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
동적 라우팅
사용 목적
2. 네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
1) UI/UX 디자인 전략
2) 기술적 최적화
preload로, 이후 이동할 가능성이 높은 페이지나 리소스는prefetch로 미리 로드합니다.3. React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.
1)
useState와useReducer를 활용한 지역 상태 관리useStateuseReducer2) Context API
useReducer와 함께 사용하면 여러 하위 컴포넌트에서 동일한 상태와 dispatch를 공유할 수 있습니다.3) 전역 상태 관리 라이브러리
정리useStateuseReducerContext API전역 상태 관리 라이브러리