Skip to content

[4주차] 이윤서 과제 제출합니다.#15

Open
yiyoonseo wants to merge 62 commits intoCEOS-Developers:masterfrom
yiyoonseo:yiyoonseo
Open

[4주차] 이윤서 과제 제출합니다.#15
yiyoonseo wants to merge 62 commits intoCEOS-Developers:masterfrom
yiyoonseo:yiyoonseo

Conversation

@yiyoonseo
Copy link
Copy Markdown

@yiyoonseo yiyoonseo commented Apr 24, 2026

🔗 배포링크

왓츠앱 리디자인

🗨️ 노션 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 (경로 파라미터)

라우트를 정의할 때 콜론(:)을 사용하여 변수명을 지정한다. 이 자리에 어떤 값이 들어오든 해당 컴포넌트를 렌더링한다.

// App.js
<Route path="/product/:id" element={<ProductDetail />} />
  • /product/1, /product/apple 등 :id 자리에 오는 값에 따라 동일한 ProductDetail 컴포넌트가 호출된다.

useParams Hook

컴포넌트 내부에서 URL에 담긴 실제 값을 가져올 때 사용한다.

// ProductDetail.jsx
import { useParams } from 'react-router-dom';

function ProductDetail() {
  const { id } = useParams(); // URL의 :id 값을 추출
  return <div>현재 보고 있는 상품 ID: {id}</div>;
}

장점

  • 유지보수 효율성: 상품이 10,000개라고 해서 페이지 컴포넌트를 10,000개 만들 필요가 없다. 하나의 템플릿 컴포넌트만 관리하면 된다.

  • 데이터 바인딩: URL에 포함된 ID 값을 이용해 서버 API(예: fetch('/api/product/' + id))를 호출하여 각기 다른 데이터를 화면에 뿌려주기 용이하다.

  • 사용자 경험: 사용자에게 의미 있는 URL(Readable URL)을 제공할 수 있으며, 특정 페이지를 공유하거나 북마크하기 편리하다.

중첩 라우팅 (Nested Routing)과 함께 사용

<Route path="/user/:username" element={<UserProfile />}>
  <Route path="posts" element={<UserPosts />} />
  <Route path="info" element={<UserInfo />} />
</Route>

이 경우 useParams를 통해 username을 공유하면서 하위의 상세 정보들만 교체하여 보여줄 수 있다.

 

네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?

UI/UX 디자인 전략

  • 스켈레톤 UI (Skeleton Screen): 데이터가 로드되기 전, 실제 콘텐츠의 윤곽(회색 박스 등)을 미리 보여줌

  • 프로그레스 바 (Progress Bar): 상단에 얇은 진행 표시줄을 두어 작업이 멈추지 않았음을 알림

  • 낙관적 업데이트 (Optimistic UI): '좋아요' 버튼 등을 누를 때 서버 응답을 기다리지 않고 즉시 성공한 것처럼 UI를 변경한 뒤, 실패 시에만 되돌림

기술적 최적화 방법

  • Code Splitting (코드 분할): React.lazySuspense를 사용하여 지금 당장 필요한 JS 코드만 먼저 다운로드

  • 이미지 최적화: WebP 포맷 사용, Lazy Loading 적용, 그리고 저해상도 이미지를 먼저 보여주는 기법을 활용

  • 데이터 캐싱: React Query나 SWR 같은 라이브러리를 사용하여 이미 불러온 데이터를 메모리에 저장해 재접속 시 즉시 보여줌

 

React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.

지역 상태 관리 (Local State Management)

컴포넌트 내부에서 생성되어 해당 컴포넌트와 그 자식 컴포넌트 내에서만 유효한 상태를 의미한다.
useState:

  • 가장 기본적인 상태 관리 훅이다.
  • 상태 업데이트 로직이 간단할 때(단순 값 변경, 불리언 전환 등) 사용한다.
  • 상태가 개별적으로 관리되므로, 여러 상태가 서로 의존적일 경우 코드가 파편화될 수 있다.

useReducer:

  • action과 reducer를 통해 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있다.
  • 다음 상태가 이전 상태에 의존하거나, 여러 개의 하위 상태를 한꺼번에 변경해야 하는 복잡한 로직에 적합하다.
  • dispatch를 통해 의도를 전달하므로 상태 변경 추적이 용이하다.

전역 상태 관리 (Global State Management)

애플리케이션 전체 혹은 여러 개의 독립적인 컴포넌트 트리에서 공유해야 하는 데이터를 다룬다.
Context API (React 내장 기능)

  • 목적: '상태 관리'보다는 **'데이터 전달(Dependency Injection)'**에 초점이 맞춰져 있다.
  • 동작: Provider 하위에 있는 컴포넌트들은 Props 없이도 데이터에 접근할 수 있다.
  • 한계: Context의 값이 바뀌면 이를 구독하는 모든 하위 컴포넌트가 리렌더링된다. 따라서 빈번하게 업데이트되는 대규모 데이터 관리에는 성능상 불리하다.

전역 상태 관리 라이브러리 (Redux, Zustand, Recoil 등)

  • 목적: 효율적인 **'상태 저장 및 최적화'**를 위해 설계되었다.
  • 특징:
    선택적 렌더링: 컴포넌트가 전역 상태 중 자신이 필요한 일부만 '구독'할 수 있어, 관련 없는 상태 변화에는 리렌더링되지 않는다.
  • 비동기 처리: 미들웨어를 통해 API 호출과 같은 비동기 로직을 체계적으로 관리한다.
  • 디버깅: 상태 변화의 이력을 기록하거나 특정 시점으로 되돌리는(Time-travel) 기능을 제공한다.

Copy link
Copy Markdown

@Wannys26 Wannys26 left a comment

Choose a reason for hiding this comment

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

이번주 과제도 고생많으셨어요!!
좋은 코드 작성해주셔서, 저도 여러 점을 다시 생각해볼 수 있었습니다!!
디자이너 분이 화면보고 정말 만족하셨을 거 같네요ㅎㅎ
그리고 지난 스터디 피드백도 잘 따라 주신거 같아서 넘 좋습니다!

다음주도 화이팅입니다~!

date: Date;
}

const ChipDate = ({ date }: ChipDateProps) => (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minor한 부분이긴 한데, 협업하실때는
export default function 혹은 화살표 함수 하나로 통일하시는 걸 고려해보셔도 좋을 거 같아용

Comment on lines +48 to +62
{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" />
</>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지금은 ProfileCard 안에서 편집 가능한 요소가 많지 않아서 크게 복잡해 보이지는 않을 수 있습니다만, 확장성을 생각하면 컴포넌트를 조금 더 나눠보는 것도 좋을 것 같습니다.

현재 ProfileCard가 상태메시지 표시/수정, 프로필 이미지 표시/수정, 사진 삭제 메뉴, 이름 표시까지 함께 담당하고 있어서,
프로필 항목이 늘어난 상태라면 isEditing ? ... : ... 분기가 컴포넌트 곳곳에 더 퍼질 수 있을 것 같아요.

예를 들어 StatusBubble, ProfilePhoto, ProfileName 정도로 분리하고,
각 컴포넌트 내부에서 필요한 편집 분기를 처리하면 ProfileCard는 전체 배치만 담당하게 되어 더 읽기 쉬운 구조가 될 것 같습니다.

Comment on lines +6 to +18
export type ChatRoom = {
id: number;
participantIds: number[];
};

export type Message = {
id: number;
chatRoomId: number;
text: string;
senderId: number;
timestamp: number;
readBy: number[];
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Message, ChatRoom, Friend 같은 도메인 타입이 store 파일 안에 정의되어 있어서,
컴포넌트나 유틸함수등이 타입만 필요해도 store 파일에 의존하게 될 거 같습니다.

타입은 store 구현보다 더 넓게 쓰이는 영역이므로
별도 types 파일로 분리하면 더 좋을거 같아요!

Comment on lines +16 to +22
<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}`)}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

div 에 onClick navigate 사용하시는것도 좋은데,
React-router-dom Link 컴포넌트(A progressively enhanced a href wrapper to enable navigation with client-side routing.)를 사용하시는 것도 좋아 보입니다!

둘다 동작상 차이는 없다만, 명시적으로 a 태그를 (Link가 a태그 상위호환) 사용하셔서 접근성 측면을 향상시켜보시는것도 좋을 거 같아용!!

https://reactrouter.com/api/components/Link

{showDeleteMenu && (
<>
<div
className="fixed inset-0 z-10"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요기서도 minor한 팁 하나 드리고자 합니다!
bubble.css 파일을 보고 인상 깊어서 드리는 팁입니당

z-index 계층(모달의 z-index, toast의 z-index 등등)도 tailwind 유틸리티 클래스로 관리하시면, z-index 값도 나름의 디자인 시스템으로 한 곳에서 중앙관리 하실 수 있습니다!
잘 와닿지 않으실텐데 아래에 잘 설명된 글 첨부드립니다!

다시 생각해 보면, 
우리는 이미 모서리의 곡률이나 폰트 크기와 같이 
재사용하는 임의의 시각적 수치를 
디자인 시스템으로 견고하게 관리하곤 합니다. 
z-index를 통한 시각적 계층 구조 또한 디자인 시스템의 일부로 포함할 수 있습니다.

https://www.poodlepoodle.me/logs/escaping-from-z-index-9999

}
}
return map;
}, [messages]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

채팅방별 최근 메시지 시간을 useMemo로 계산한 점이 좋은 것 같아요!
메시지 목록이 변경될 때만 재계산되도록 되어 있어서, 채팅방 리스트 렌더링 시 불필요한 반복 연산을 줄일 수 있는 구조네요!

? { ...m, readBy: [...m.readBy, nextId] }
: m,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zustand를 활용해서 메시지, 채팅방, 사용자 상태를 한 곳에서 관리하셨네요! 덕분에 코드가 간결해서 가독성이 좋았습니다. 채팅처럼 여러 컴포넌트에서 동일한 상태를 공유하는 경우에 Context API 보다 좋은 선택인 것 같아요!

users={users}
messages={messages}
/>
<ChatRoomItem key={room.id} chatRoom={room} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 }) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

map함수 써서 내비게이션바 코드를 간소화한게 좋았다고 생각해요. 같은 코드를 6번(홈, chat, profile 세 개의 on/off) 쓰는 것보다 훨씬 좋아요~~~

}: PageHeaderProps) {
const navigate = useNavigate();

const handleBack = onBack ?? (() => navigate(-1));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

소규모 앱 입장에서는 navigate를 -1 말고 절대 경로로 두는 것도 고려해보셨으면 해요(꼭 필요한 건 아니지만, 나중에 실제 배포를 진행했을 때 풀백 없이 navigate(-1)혹은 router.push(-1)을 사용하면 오류가 날 수도 있어요)

}
}, [openId]);

// 공통 드래그 시작
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://dndkit.com/
드래그 라이브러리 문서입니다~~~

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.

4 participants