Skip to content

Conversation

@hammoooo
Copy link

@hammoooo hammoooo commented Oct 25, 2025

배포링크

https://react-messenger-22nd-ecru.vercel.app/

느낀/배운점

  1. 저는 항상 shadcn같은 CSS라이브러리를 가져다 쓰는 데 익숙했지만 이번에는 직접 디테일하게 Tailwind만으로 디자인을 구현한 것이 가장 크게 배운 점인 것 같습니다.

  2. margin, gap, 안전영역 등등 각각을 어떤 맥락에서 쓸 지 이해했으며, 부모와의 관계도 고려해야 의도치 않은 흰색 여백 같은 것들을 피할 수 있다는 것을 배웠습니다.




Key Questions

React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?

동적 라우팅 (Dynamic Routing)이란

페이지의 형식은 같지만, 그 내용만 바뀌는 경우에 사용합니다. 예를 들어 사용자 프로필 페이지의 URL을 /users/:userId와 같이 사용합니다.

이때 :userId 부분이 동적 파라미터(변수)이며 사용자가 /users/123로 접속하든 /users/321으로 접속하든 React Router는 동일한 UserProfile 컴포넌트를 렌더링합니다.

그리고 컴포넌트 내에서 useParams라는 Hook을 사용해 :userId 파라미터 값을 꺼내 사용할 수 있습니다.



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

UI/UX 디자인 전략- 스켈레톤UI

단순히 로딩 스피너를 보여주는 것은 오히려 사용자를 지치게 할 수 있습니다. 따라서 곧 보여질 것 처럼 데이터가 들어올 레이아웃을 미리 (회색) 박스 등으로 보여줍니다.

추가로, 댓글과 같은 기능은 서버에 업로드 하기전에 로컬 데이터로 이미 댓글이 달린 것처럼 보여줄 수 도 있습니다.


기술적 최적화 방법- 이미지 리사이징

아무래도 이미지가 전체 api요청에서 가장 큰 용량을 차지하고 있을 것입니다. 이때 이미지를 브라우저나 모바일 환경에 맞추어 리사이징한 이미지를 전달해준다면 이미지 크기를 효과적으로 줄일 수 있습니다.



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

  1. 지역 상태 관리 (useState, useReducer)

"컴포넌트의 개개인 저장소"

상태가 특정 컴포넌트 내부에서만 필요하거나 부모/자식 관계처럼 1~2단계 아래로만 전달(props)될 때 사용합니다.
다른 컴포넌트와 격리되어 있어 관리가 쉽습니다.

useReducer를 사용하는 이유는 useState로 관리하기엔 상태 로직이 복잡할 때 사용합니다.
상태를 변경하는 Action을 미리 정의하고 Reducer에 따라서만 상태를 업데이트합니다.
useState보다 코드는 길어지지만 상태 변화를 예측하기 쉬워집니다.



2. 전역 상태 관리 (Context API,Zustand)


"애플리케이션의 공용 저장소"

앱의 여러 컴포넌트가 공유해야 하는 상태일때 사용합니다. 예를들어 로그인한 사용자 정보, 테마(다크/라이트 모드), 장바구니 목록등이 있습니다.


지역 상태만 쓰면, 저 멀리 떨어진 컴포넌트에 데이터를 전달하기 위해 중간의 모든 컴포넌트를 거쳐 props를 전달해야 합니다. 이를 Prop Drilling이라고 하며, 매우 비효율적인 코드가 됩니다.

Copy link

@dragunshin dragunshin left a comment

Choose a reason for hiding this comment

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

컴포넌트 분리가 깔끔해서 코드가 어떤 것을 의미하는지 보기 편했습니다 깔끔한 컴포넌트 구성을 배우고 갑니다 고생하셨습니다!

<img src="/images/nnotch.svg" />
<ChatHeader />

<main className="!pb-[calc(84px+env(safe-area-inset-bottom))]">

Choose a reason for hiding this comment

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

직관적인 safe area 처리같은데 css변수로 처리하면 관리에 유용할 것 같아요

//import { shallow } from "zustand/shallow";

export default function ChattingList() {
const init = useChat((s) => s.init);

Choose a reason for hiding this comment

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

useChat 여러개를 사용하는 부분에 대해서 zustand의 shallow를 사용하는 방식으로 렌더링 여러번을 줄일 수 있다고 합니다

shallow
관련된 링크라 같이 참고해 보면 좋을 것 같아요

<span className="text-[11px] text-gray-400">{item.timeLabel}</span>
{item.unread > 0 ? (
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-[11px] font-semibold text-white">
{item.unread > 99 ? "99+" : item.unread}

Choose a reason for hiding this comment

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

99개가 넘어가면 99+ 처리를 해놓은것이 되게 디테일한 것 같아요

import Coupon from "@/assets/coupon.svg?react";
import ProfileGift from "@/assets/profile/profileGift.svg?react";

export default function FriendsList() {

Choose a reason for hiding this comment

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

여기도 shallow 써보면 좋을것 같아요!

[categoryOrder, dynamicKeys]
);

return (

Choose a reason for hiding this comment

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

목록 기반이면 ul을 써보는 것도 하나의 방법같아요

f.id === userId ? { ...f, groups: unique } : f
),
});
const unknown = unique.filter((g) => !get().categoryOrder.includes(g));

Choose a reason for hiding this comment

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

그룹 발견시 추가하는 방식이 좋은 것 같아요. 즐겨찾기 같은 기능에도 도움이 될 것 같아요

const { friends, query, activeCollection } = get();
const q = query.trim().toLowerCase();

let list = friends.filter(

Choose a reason for hiding this comment

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

현재 검색이 name status만 가능한데 전화번호같은 다른 검색으로도 가능하면 좋을 것 같아요

Copy link
Member

@chaeyoungwon chaeyoungwon left a comment

Choose a reason for hiding this comment

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

과제하시느라 수고 많으셨어요! 🙌
현재 채팅 페이지 등 루트가 아닌 경로에서 새로고침 시 404 에러가 발생하는 것 같아요.
vercel.json을 추가하셔서, 지난 3주차 과제 피드백 노션에 안내된 설정을 참고해 적용해보시면 좋을 것 같습니다
다음 과제도 파이팅입니다!

Comment on lines +54 to +65
--text-headline-1: 24px;
--text-headline-2: 22px;
--text-headline-3: 20px;

--text-body-1: 18px; /* Semibold, 120%, -2% letter-spacing */
--text-body-2: 18px; /* Regular/Medium, 120%, -2% */
--text-body-3: 16px; /* Semibold, 120%, -2% */
--text-body-4: 16px; /* Medium, 120%, -2% */
--text-body-5: 14px; /* Semibold, 130% */
--text-body-6: 14px; /* Medium, 130% */

--text-caption: 12px; /* Regular, 130% */
Copy link
Member

Choose a reason for hiding this comment

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

typography는 글자 크기만 지정하기보단,
@layer utilities를 활용해 폰트 크기, 굵기, 줄 간격 같은 속성까지 한 번에 정의해두면 좋습니당

컴포넌트에서 일일이 스타일 지정 안 해도 되고,
전체적으로 통일감 있는 디자인을 유지할 수 있습니다!
다른 분들 코드도 참고해보시면 좋을 거 같아요!

Image

Copy link
Member

Choose a reason for hiding this comment

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

해당 이미지를 사용하지 않고, 하나의 아이콘만 활용해서 색상을 랜덤으로 지정해주는 방식도 괜찮을 것 같아요!

Copy link
Member

Choose a reason for hiding this comment

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

현재 중복된 이미지들이 꽤 포함되어 있는 것 같아요 !!
사용 중인 이미지만 남기고 불필요한 이미지는 정리해주시면 좋을 것 같습니다 :)

또한, 직접 구현 가능한 텍스트는 이미지 대신 코드로 작성해주시는 걸 추천드려요!
초기 렌더링 시 텍스트가 더 빠르게 표시될 뿐만 아니라, 텍스트를 수정하기 위해 이미지를 교체할 필요가 없고
전체 프로젝트 용량도 줄일 수 있으니까요!

Comment on lines +9 to +23
export const api = {
users: {
list: () => http<any[]>("/data/users.json"),
},
collections: {
list: () => http<any[]>("/data/collections.json"),
},
messages: {
list: () => http<Record<string, any[]>>("/data/messages.json"),
},
rooms: {
list: () => http<any[]>("/data/room.json"),
},
// messages는 localStorage 이용
};
Copy link
Member

Choose a reason for hiding this comment

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

any 타입 대신 실제 데이터 구조에 맞는 타입을 지정해주시면 더 좋을 거 같아요!

Comment on lines +14 to +20
const tabs: { key: ActiveKey; src: string; alt: string }[] = [
{ key: "friends", src: "/images/tab/tabFriends.svg", alt: "친구" },
{ key: "chat", src: "/images/tab/tabChatting.svg", alt: "채팅" },
{ key: "openchat", src: "/images/tab/tabOpenChat.svg", alt: "오픈채팅" },
{ key: "shopping", src: "/images/tab/tabShopping.svg", alt: "쇼핑" },
{ key: "more", src: "/images/tab/tabMore.svg", alt: "더보기" },
];
Copy link
Member

Choose a reason for hiding this comment

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

constants 폴더로 분리하면 좋을 거 같아용

Copy link
Member

Choose a reason for hiding this comment

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

추가로 라우팅 경로까지 넣어주면 리턴문 안에서 더 간단히 작성하실 수 있을 거 같네용

import type { User } from "@/types";
//import Avatar from "../../components/Avatar";

export default function FriendsRow({ f }: { f: User }) {
Copy link
Member

Choose a reason for hiding this comment

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

단순 map 콜백 내부에서는 축약형 변수를 사용해도 괜찮지만,
컴포넌트의 props로 전달되는 경우에는 의미가 명확한 변수명을 사용하는 것이 더 좋을 거 같네요!

Comment on lines +28 to +30
<p className="truncate text-[13px] text-gray-600">
{item.lastText}
</p>
Copy link
Member

Choose a reason for hiding this comment

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

목데이터를 기반으로 마지막 텍스트를 설정해주셨는데,
사용자가 입력한 내용이 로컬 스토리지에 저장된다면,
해당 저장값을 불러와 마지막 텍스트로 표시하는 방식도 좋을 것 같아요!

Comment on lines +65 to +82
if (loading) {
return (
<div className="mx-auto min-h-screen w-[375px] border border-gray-200 bg-white">
<main className="divide-y divide-gray-200">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-4 py-3">
<div className="h-12 w-12 rounded-xl bg-gray-200 animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-3 w-1/3 bg-gray-200 animate-pulse rounded" />
<div className="h-3 w-2/3 bg-gray-200 animate-pulse rounded" />
</div>
<div className="h-3 w-8 bg-gray-200 animate-pulse rounded" />
</div>
))}
</main>
</div>
);
}
Copy link
Member

Choose a reason for hiding this comment

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

Image

스켈레톤 UI도 추가해주셨네요 👍
개발자 도구의 네트워크 탭에서 속도를 느리게 설정하면 로딩 상황을 직접 테스트할 수 있으니,
테스트하면서 스켈레톤 간 간격이나 정렬을 조금 조정해보셔도 좋을 것 같아요!

Comment on lines +25 to +29
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (el) el.scrollTop = el.scrollHeight;
}, [list.length]);
Copy link
Member

Choose a reason for hiding this comment

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

흠 현재 스크롤이 맨 아래로 자동으로 내려가지 않는 같은데, 다시 한 번 확인해보시면 좋을 거 같아요!


export default function App() {
return (
<div className="mx-auto w-[375px] h-[812px] bg-white ">
Copy link
Member

Choose a reason for hiding this comment

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

앞으로 진행하실 프로젝트도 웹앱 형태일 수도 있을 거 같은데요,
화면의 전체 너비를 기준으로 중앙에 위치하도록 작업해주시면 좋을 거 같아요!

Image

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