[4주차] 권오진 과제 제출합니다.#11
Conversation
waldls
left a comment
There was a problem hiding this comment.
4주차 과제 수고 많으셨습니다!
노션에서 디자이너님께 로컬스토리지 비우는 방법을 상세하게 설명해주신 점이 인상적이었어요 !!
다음 주 페어 과제도 열심히 해봅시다 🙌🏻
| <CurrentPlace /> | ||
| <AlarmBox /> | ||
|
|
||
| <main className="flex w-[var(--screen-width)] flex-col items-start gap-[12px] pt-[8px] overflow-y-auto"> |
There was a problem hiding this comment.
arbitrary value대신 Tailwind 기본 Spacing을 활용해보시면 좋을 것 같습니다!
gap-[12px] -> gap-3, pt-[8px] -> pt-2 처럼 4로 나눈 값으로 바로 사용할 수 있습니다!
| function formatListTime(sentAt: number | null) { | ||
| if (!sentAt) return ""; | ||
|
|
||
| const now = Date.now(); | ||
| const diff = now - sentAt; | ||
|
|
||
| const minute = 1000 * 60; | ||
| const hour = minute * 60; | ||
| const day = hour * 24; | ||
|
|
||
| if (diff < hour) { | ||
| const minutes = Math.max(1, Math.floor(diff / minute)); | ||
| return `${minutes}분`; | ||
| } | ||
|
|
||
| if (diff < day) { | ||
| const hours = Math.max(1, Math.floor(diff / hour)); | ||
| return `${hours}시간`; | ||
| } | ||
|
|
||
| return ""; | ||
| } |
| if (isMe) { | ||
| if (isConnectedToPrevious && !isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[20px_4px_20px_20px]"; | ||
| } else if (!isConnectedToPrevious && isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[20px_20px_4px_20px]"; | ||
| } else if (isConnectedToPrevious && isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[20px_4px_4px_20px]"; | ||
| } else { | ||
| bubbleRadiusClass = "rounded-[20px]"; | ||
| } | ||
| } else { | ||
| if (isConnectedToPrevious && !isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[4px_20px_20px_20px]"; | ||
| } else if (!isConnectedToPrevious && isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[20px_20px_20px_4px]"; | ||
| } else if (isConnectedToPrevious && isConnectedToNext) { | ||
| bubbleRadiusClass = "rounded-[4px_20px_20px_4px]"; | ||
| } else { | ||
| bubbleRadiusClass = "rounded-[20px]"; | ||
| } | ||
| } |
There was a problem hiding this comment.
if/else 중첩이 깊고 조건 조합이 8개여서, 한눈에 파악이 어려운 것 같습니다!
lookup table로 정리하고, Tailwind v4 @theme에 radius 토큰을 등록하면 아래처럼 간단하게 쓸 수 있을 것 같아요!
type ConnectionKey = "00" | "01" | "10" | "11";
const BUBBLE_RADIUS: Record<"me" | "other", Record<ConnectionKey, string>> = {
me: {
"11": "rounded-tl-20 rounded-tr-4 rounded-br-4 rounded-bl-20",
"10": "rounded-tl-20 rounded-tr-4 rounded-br-20 rounded-bl-20",
"01": "rounded-tl-20 rounded-tr-20 rounded-br-4 rounded-bl-20",
"00": "rounded-20",
},
other: {
"11": "rounded-tl-4 rounded-tr-20 rounded-br-20 rounded-bl-4",
"10": "rounded-tl-4 rounded-tr-20 rounded-br-20 rounded-bl-20",
"01": "rounded-tl-20 rounded-tr-20 rounded-br-20 rounded-bl-4",
"00": "rounded-20",
},
};
const key = `${+isConnectedToPrevious}${+isConnectedToNext}` as ConnectionKey;
const bubbleRadiusClass = BUBBLE_RADIUS[isMe ? "me" : "other"][key];| /* typography */ | ||
| --text-xs: 12px; | ||
| --text-sm: 14px; | ||
| --font-weight-regular: 400; | ||
| --font-weight-semibold: 600; | ||
| --line-height-tight: 140%; |
| import { useEffect, useMemo, useState } from "react"; | ||
| import AppBarChatRoom from "../components/chat-page/AppBarChatRoom"; | ||
| import MessageNavBar from "../components/chat-page/MessageNavBar"; | ||
| import MessageSend, { | ||
| type ChatMessage, | ||
| } from "../components/chat-page/MessageSend"; | ||
| import profile from "../assets/chat-page/profile.svg"; | ||
| import rawMessages from "../data/messages.json"; | ||
| import rawUsers from "../data/users.json"; | ||
| import rawChatRooms from "../data/chatRooms.json"; | ||
| import { useParams } from "react-router-dom"; |
There was a problem hiding this comment.
절대 경로 설정을 하면 depth가 깊어질수록 가독성이 떨어지는 문제를 해결할 수 있을 것 같습니다!
설정 방법 관련 블로그 글 하나 남깁니닷 !!
|
|
||
| function PlaceHolder() { | ||
| return ( |
There was a problem hiding this comment.
해당 컴포넌트는 채팅 리스트 페이지에서 사람 또는 채팅방을 검색하는 컴포넌트이므로,
PlaceHolder 라는 컴포넌트명 대신 SearchInput 또는 ChatSearchInput으로 사용하시면 훨씬 직관적일 것 같습니다!
| <img | ||
| src={newChat} | ||
| alt="newChat" | ||
| className="w-[var(--size-24)] h-[var(--size-24)] shrink-0" | ||
| /> |
There was a problem hiding this comment.
아이콘 SVG 파일에 SVGR 설정을 적용하면 <NewChat className="size-6 shrink-0" /> 형태로 간결하게 사용할 수 있을 것 같아요!
ryu-won
left a comment
There was a problem hiding this comment.
고생많으셨어요!! 디자이너분과도 소통하시면서 열심히 QA하신 것 같습니다! 시험 기간이셨을텐데 고생많으셨어요👍
| <div> | ||
| <div className="flex w-[var(--screen-width)] flex-col items-start border-t border-t-[size:1px] border-t-[color:var(--color-grey-300)] bg-[color:var(--color-grey-50)]"> | ||
| <section className="flex self-stretch items-center justify-between px-[var(--space-12)]"> | ||
| <button className="flex h-[44px] w-[44px] items-center justify-center gap-[var(--space-10)]"> |
| function App() { | ||
| return ( | ||
| <div className="flex min-h-screen w-full items-center justify-center"> | ||
| <div className="relative flex h-[var(--screen-height)] w-[var(--screen-width)] flex-col"> |
There was a problem hiding this comment.
| }; | ||
|
|
||
| return ( | ||
| <div className="inline-flex w-[var(--screen-width)] items-center gap-[var(--space-8)] px-[var(--space-12)] pt-[var(--space-10)] pb-[var(--space-40)]"> |
There was a problem hiding this comment.
pb-[var(--space-40)]이 하드코딩되어 있어서,
iOS Safe Area(기기별 Home Indicator 영역)가 반영되지 않습니다.
env(safe-area-inset-bottom)으로 대응해주시면 좋을 것 같습니다!
단, index.html의 viewport meta에 viewport-fit=cover 추가가 선행되어야 합니다.
| import send from "../../assets/chat-list/send.svg"; | ||
| import navProfile from "../../assets/chat-list/nav-profile.svg"; | ||
|
|
||
| function NavBarChatList() { |
There was a problem hiding this comment.
button 안에 이미지가 있는 동일구조가 반복되고 있는데 map 메서드로 간결하게 해주셔도 될 듯합니다!
{NAV_ITEMS.map(({ src, alt, path }) => (
<button
key={alt}
onClick={() => navigate(path)}
className="flex h-[44px] w-[44px] items-center justify-center"
>
<img
src={src}
alt={alt}
className={`size-[var(--size-24)] shrink-0 transition-opacity duration-150 ${
location.pathname === path
? "opacity-100" // 현재 탭
: "opacity-40" // 비활성 탭
}`}
/>
</button>
))}
|
오진님 4주차 과제도 수고많으셨습니다! 현재 배포링크를 실행했을 때 채팅방에서는 마지막 메시지가 localStorage에 정상적으로 반영되지만, |


배포링크
피그마 링크
노션 링크
느낀점
Review Question
React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
React Router : React 애플리케이션에서 클라이언트 측 라우팅을 쉽게 구현할 수 있도록 도와주는 라이브러리
→ React는 기본적으로 SPA이고 URL이 변경되더라도 페이지 전체를 새로고침하지 않고도 컴포넌트만 변경할 수 있도록 설계되어 있음
React Router를 사용한단면 사용자가 특정 URL로 이동할 때 해당 URL에 맞는 컴포넌트를 렌더링함
동적 라우팅 : 웹 애플리케이션에서 클라이언트의 요청에 따라 경로를 동적으로 처리하는 라우팅 방식
사용자의 입력, 상태 변화, 다양한 조건에 따라 서버가 어떤 페이지나 리소스를 제공할지 결정
경로변수 (Parameters)를 사용해 동적페이지를 만들 수 있음
useParams → React Router에서 URL의 파라미터 값을 가져오는 Hook
네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
UI/UX 디자인 전략
느린 네트워크에서 사용자가 앱이 작동하지 않다고 느끼지 않도록 로딩상태를 명확하게 보여주어야 합니다.
모든 콘텐츠를 한 번에 보여주려고 하면 초기 화면이 늦게 뜰 수 있어 사용자가 먼저 봐야 하는 정보를 우선으로 제공해야 합니다.
데이터를 불러오는 동안 빈 화면이 보이면 오류라고 사용자가 생각할 수 있으므로 추가적인 UI를 제공하거나 화면 구조를 먼저 보여주며 사용자 경험을 개선합니다.
느린 네트워크에서 요청 실패가 자주 발생할 수 있으므로 요청 실패에 대한 원인과 해결 방안을 제시하여 에러 상황을 안내하고 해결할 수 있도록 도와줍니다.
기술적 최적화 방법
이미지는 웹페이지에서 용량을 많이 차지하는 요소로 이미지 압축, 반응형 이미지 등 용량을 감소하며 품질을 해치지 않도록 하는 것이 중요합니다.
디자인 전략과 동일하게 처음부터 모든 데이터를 불러오지 않고, 필요한 시점에 필요한 정보를 불러오는 방식을 활용합니다.
캐싱을 사용하여 이미 불러온 데이터를 매번 다시 요청하지 않도록 저장해두는 방식을 사용합니다.
필요한 데이터만 요청하거나 여러 API 호출을 묶는 등 요청 횟수를 줄여 네트워크가 느린 환경에도 작동하도록 할 수 있습니다.
→UI/UX 디자인 전략은 네트워크 속도가 느린 상황에서도 사용자가 불편함을 덜 느끼도록 하며 기술적 최적화 방법은 실제로 필요한 데이터를 더 적고, 더 빠르게 불러오도록 만드는 것이 중요합니다.
지역 상태 관리 : 특정 컴포넌트 또는 가까운 하위 컴포넌트에서만 필요한 상태
useState : 단순한 값이나 짧은 상태를 관리할 때 적합
→ 상태가 단순할 때, 업데이트 로직이 간단할 때, 한 컴포넌트 안에서만 사용할 때
useReducer : useState보다 상태 변경 로직이 복잡할 때 사용
→ 상태 값이 여러 개일 때, 상태 변경 로직이 복잡할 때, 이전 상태를 기반으로 변경할 때, 상태 변화 종류가 많을 때
Context API : 여러 컴포넌트가 같은 값을 공유할 수 있도록 함
일반적으로 부모에서 자식 컴포넌트로 props를 사용하여 데이터를 전달할 때 구조가 깊어지면 여러 단계를 거쳐 계속 내려야 하는데 Context API는 필요한 컴포넌트가 직접 값을 가져올 수 있게 함
전역 상태 관리 라이브러리 : 여러 컴포넌트, 여러 페이지에서 공유하는 상태를 더 체계적으로 관리하기 위한 도구
→ 여러 페이지에서 같은 상태를 공유, 상태 변경 로직이 복잡, 상태를 디버깅해야 할 때, 서버 데이터와 클라이언트 상태가 섞였을 때, 상태 변경 추적이 중요할 때
ex) Zustand를 사용하면 전역 store를 만들 수 있으며 컴포넌트 깊이에 상관없이 필요한 곳에서 상태를 가져올 수 있음