[4주차] 이윤서 과제 제출합니다.#15
Conversation
…mName utility, remove User type
Wannys26
left a comment
There was a problem hiding this comment.
이번주 과제도 고생많으셨어요!!
좋은 코드 작성해주셔서, 저도 여러 점을 다시 생각해볼 수 있었습니다!!
디자이너 분이 화면보고 정말 만족하셨을 거 같네요ㅎㅎ
그리고 지난 스터디 피드백도 잘 따라 주신거 같아서 넘 좋습니다!
다음주도 화이팅입니다~!
| date: Date; | ||
| } | ||
|
|
||
| const ChipDate = ({ date }: ChipDateProps) => ( |
There was a problem hiding this comment.
minor한 부분이긴 한데, 협업하실때는
export default function 혹은 화살표 함수 하나로 통일하시는 걸 고려해보셔도 좋을 거 같아용
| {isEditing ? ( | ||
| <> | ||
| <div className="relative z-10 inline-block"> | ||
| <span className="invisible inline-block text-body-01 py-4 px-10 whitespace-pre min-w-31.5"> | ||
| {statusMessage || "상태를 입력해주세요"} | ||
| </span> | ||
| <input | ||
| className="absolute inset-0 w-full text-body-01 text-gray-06 text-center rounded-[52px] outline-none bg-main-bg" | ||
| value={statusMessage ?? ""} | ||
| onChange={(e) => onStatusMessageChange?.(e.target.value)} | ||
| placeholder="상태를 입력해주세요" | ||
| /> | ||
| </div> | ||
| <EditTail className="w-8 absolute left-1/4 -bottom-5.5 z-0" /> | ||
| </> |
There was a problem hiding this comment.
지금은 ProfileCard 안에서 편집 가능한 요소가 많지 않아서 크게 복잡해 보이지는 않을 수 있습니다만, 확장성을 생각하면 컴포넌트를 조금 더 나눠보는 것도 좋을 것 같습니다.
현재 ProfileCard가 상태메시지 표시/수정, 프로필 이미지 표시/수정, 사진 삭제 메뉴, 이름 표시까지 함께 담당하고 있어서,
프로필 항목이 늘어난 상태라면 isEditing ? ... : ... 분기가 컴포넌트 곳곳에 더 퍼질 수 있을 것 같아요.
예를 들어 StatusBubble, ProfilePhoto, ProfileName 정도로 분리하고,
각 컴포넌트 내부에서 필요한 편집 분기를 처리하면 ProfileCard는 전체 배치만 담당하게 되어 더 읽기 쉬운 구조가 될 것 같습니다.
| export type ChatRoom = { | ||
| id: number; | ||
| participantIds: number[]; | ||
| }; | ||
|
|
||
| export type Message = { | ||
| id: number; | ||
| chatRoomId: number; | ||
| text: string; | ||
| senderId: number; | ||
| timestamp: number; | ||
| readBy: number[]; | ||
| }; |
There was a problem hiding this comment.
Message, ChatRoom, Friend 같은 도메인 타입이 store 파일 안에 정의되어 있어서,
컴포넌트나 유틸함수등이 타입만 필요해도 store 파일에 의존하게 될 거 같습니다.
타입은 store 구현보다 더 넓게 쓰이는 영역이므로
별도 types 파일로 분리하면 더 좋을거 같아요!
| <div | ||
| className={clsx( | ||
| "flex flex-row items-center gap-4 py-2.5 cursor-pointer", | ||
| isMe ? "px-5 border-b border-gray-03" : "px-4", | ||
| )} | ||
| onClick={() => navigate(isMe ? "/profile" : `/profile/${id}`)} | ||
| > |
There was a problem hiding this comment.
div 에 onClick navigate 사용하시는것도 좋은데,
React-router-dom Link 컴포넌트(A progressively enhanced a href wrapper to enable navigation with client-side routing.)를 사용하시는 것도 좋아 보입니다!
둘다 동작상 차이는 없다만, 명시적으로 a 태그를 (Link가 a태그 상위호환) 사용하셔서 접근성 측면을 향상시켜보시는것도 좋을 거 같아용!!
| {showDeleteMenu && ( | ||
| <> | ||
| <div | ||
| className="fixed inset-0 z-10" |
There was a problem hiding this comment.
요기서도 minor한 팁 하나 드리고자 합니다!
bubble.css 파일을 보고 인상 깊어서 드리는 팁입니당
z-index 계층(모달의 z-index, toast의 z-index 등등)도 tailwind 유틸리티 클래스로 관리하시면, z-index 값도 나름의 디자인 시스템으로 한 곳에서 중앙관리 하실 수 있습니다!
잘 와닿지 않으실텐데 아래에 잘 설명된 글 첨부드립니다!
다시 생각해 보면,
우리는 이미 모서리의 곡률이나 폰트 크기와 같이
재사용하는 임의의 시각적 수치를
디자인 시스템으로 견고하게 관리하곤 합니다.
z-index를 통한 시각적 계층 구조 또한 디자인 시스템의 일부로 포함할 수 있습니다.
| } | ||
| } | ||
| return map; | ||
| }, [messages]); |
There was a problem hiding this comment.
채팅방별 최근 메시지 시간을 useMemo로 계산한 점이 좋은 것 같아요!
메시지 목록이 변경될 때만 재계산되도록 되어 있어서, 채팅방 리스트 렌더링 시 불필요한 반복 연산을 줄일 수 있는 구조네요!
| ? { ...m, readBy: [...m.readBy, nextId] } | ||
| : m, | ||
| ); | ||
|
|
There was a problem hiding this comment.
Zustand를 활용해서 메시지, 채팅방, 사용자 상태를 한 곳에서 관리하셨네요! 덕분에 코드가 간결해서 가독성이 좋았습니다. 채팅처럼 여러 컴포넌트에서 동일한 상태를 공유하는 경우에 Context API 보다 좋은 선택인 것 같아요!
| users={users} | ||
| messages={messages} | ||
| /> | ||
| <ChatRoomItem key={room.id} chatRoom={room} /> |
There was a problem hiding this comment.
props drilling은 상위 컴포넌트에서 하위 컴포넌트로 동일한 상태를 지속적으로 전달할 때(부모-자식 컴포넌트가 2개 이상인 상황에서) 발생하는 문제이다 보니, 조금 엄밀히 말하자면 'props drilling 개선'보다는 '상태 조회 위치 변경'이 정확해 보입니다!
|
|
||
| return ( | ||
| <div className="flex flex-row justify-around items-center px-8 py-3 bg-white border-t border-gray-02"> | ||
| {NAV_ITEMS.map(({ path, Default, Active }) => { |
There was a problem hiding this comment.
map함수 써서 내비게이션바 코드를 간소화한게 좋았다고 생각해요. 같은 코드를 6번(홈, chat, profile 세 개의 on/off) 쓰는 것보다 훨씬 좋아요~~~
| }: PageHeaderProps) { | ||
| const navigate = useNavigate(); | ||
|
|
||
| const handleBack = onBack ?? (() => navigate(-1)); |
There was a problem hiding this comment.
소규모 앱 입장에서는 navigate를 -1 말고 절대 경로로 두는 것도 고려해보셨으면 해요(꼭 필요한 건 아니지만, 나중에 실제 배포를 진행했을 때 풀백 없이 navigate(-1)혹은 router.push(-1)을 사용하면 오류가 날 수도 있어요)
| } | ||
| }, [openId]); | ||
|
|
||
| // 공통 드래그 시작 |
🔗 배포링크
왓츠앱 리디자인
🗨️ 노션 QA 링크
노션
🎨 피그마 링크
피그마
리팩토링
🔧 코드 품질 / 관심사 분리
ChatDate 날짜 포맷 로직 유틸 분리 — 컴포넌트 내부 인라인 로직 → formatTime.ts의 getFormattedDate로 추출 (리뷰어 의견 직접 반영)
스와이프 로직 분리 — ChatRoomItem 내부 → useSwipeGesture 훅으로 추출
채팅방 필터/정렬 로직 분리 — ChatList 내부 → useFilteredChatRooms 훅으로 추출
메시지 그룹핑 로직 분리 — ChatRoom 내부 → groupMessages 순수 함수로 추출
♻️ 중복 제거
roomName 중복 로직 통합 — ChatRoomItem + ChatRoom 각각에 동일한 filter+map+join → getRoomName 유틸로 통일
아바타 렌더링 패턴 통합 — Friend/ProfileCard/ChatThumbnail 세 곳의 제각각 구현 → Avatar 공통 컴포넌트로 통일
🚫 매직 넘버 제거
하드코딩된 1 (내 userId) → MY_ID 상수로 중앙화 (src/constants/userId.ts)
🐛 버그 수정
swapPerspective — users.find(u => u.id !== currentId)가 채팅방 참여자가 아닌 엉뚱한 유저를 선택할 수 있는 버그 수정
markAsRead — MY_ID로 체크하면서 1을 push하는 불일치 버그 수정
🗑️ 타입 정리
User 타입 / users 배열 — chat store에서 제거 (friends store로 일원화)
✨ 기능 추가
단체 채팅방 버블 위 발신자 이름 표시
채팅방 목록 스와이프로 즐겨찾기 추가/해제
마지막 메시지 날짜 포맷 개선 (오늘 → 시간, 그 외 → "N월 N일")
PageHeader sticky 문제 해결 (스크롤 영역 밖으로 이동)
🏷️ Review Question
React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
Path Parameters (경로 파라미터)
라우트를 정의할 때 콜론(:)을 사용하여 변수명을 지정한다. 이 자리에 어떤 값이 들어오든 해당 컴포넌트를 렌더링한다.
/product/1,/product/apple등 :id 자리에 오는 값에 따라 동일한 ProductDetail 컴포넌트가 호출된다.useParams Hook
컴포넌트 내부에서 URL에 담긴 실제 값을 가져올 때 사용한다.
장점
유지보수 효율성: 상품이 10,000개라고 해서 페이지 컴포넌트를 10,000개 만들 필요가 없다. 하나의 템플릿 컴포넌트만 관리하면 된다.
데이터 바인딩: URL에 포함된 ID 값을 이용해 서버 API(예: fetch('/api/product/' + id))를 호출하여 각기 다른 데이터를 화면에 뿌려주기 용이하다.
사용자 경험: 사용자에게 의미 있는 URL(Readable URL)을 제공할 수 있으며, 특정 페이지를 공유하거나 북마크하기 편리하다.
중첩 라우팅 (Nested Routing)과 함께 사용
이 경우
useParams를 통해username을 공유하면서 하위의 상세 정보들만 교체하여 보여줄 수 있다.네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
UI/UX 디자인 전략
스켈레톤 UI (Skeleton Screen): 데이터가 로드되기 전, 실제 콘텐츠의 윤곽(회색 박스 등)을 미리 보여줌
프로그레스 바 (Progress Bar): 상단에 얇은 진행 표시줄을 두어 작업이 멈추지 않았음을 알림
낙관적 업데이트 (Optimistic UI): '좋아요' 버튼 등을 누를 때 서버 응답을 기다리지 않고 즉시 성공한 것처럼 UI를 변경한 뒤, 실패 시에만 되돌림
기술적 최적화 방법
Code Splitting (코드 분할):
React.lazy와Suspense를 사용하여 지금 당장 필요한 JS 코드만 먼저 다운로드이미지 최적화: WebP 포맷 사용, Lazy Loading 적용, 그리고 저해상도 이미지를 먼저 보여주는 기법을 활용
데이터 캐싱: React Query나 SWR 같은 라이브러리를 사용하여 이미 불러온 데이터를 메모리에 저장해 재접속 시 즉시 보여줌
React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.
지역 상태 관리 (Local State Management)
컴포넌트 내부에서 생성되어 해당 컴포넌트와 그 자식 컴포넌트 내에서만 유효한 상태를 의미한다.
useState:
useReducer:
전역 상태 관리 (Global State Management)
애플리케이션 전체 혹은 여러 개의 독립적인 컴포넌트 트리에서 공유해야 하는 데이터를 다룬다.
Context API (React 내장 기능)
전역 상태 관리 라이브러리 (Redux, Zustand, Recoil 등)
선택적 렌더링: 컴포넌트가 전역 상태 중 자신이 필요한 일부만 '구독'할 수 있어, 관련 없는 상태 변화에는 리렌더링되지 않는다.