Skip to content

[4주차] 권오진 과제 제출합니다.#11

Open
KOJ50 wants to merge 24 commits intoCEOS-Developers:masterfrom
KOJ50:KOJ50
Open

[4주차] 권오진 과제 제출합니다.#11
KOJ50 wants to merge 24 commits intoCEOS-Developers:masterfrom
KOJ50:KOJ50

Conversation

@KOJ50
Copy link
Copy Markdown

@KOJ50 KOJ50 commented Apr 7, 2026

배포링크
피그마 링크
노션 링크


느낀점


Review Question

  1. React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
    React Router : React 애플리케이션에서 클라이언트 측 라우팅을 쉽게 구현할 수 있도록 도와주는 라이브러리
    → React는 기본적으로 SPA이고 URL이 변경되더라도 페이지 전체를 새로고침하지 않고도 컴포넌트만 변경할 수 있도록 설계되어 있음
    React Router를 사용한단면 사용자가 특정 URL로 이동할 때 해당 URL에 맞는 컴포넌트를 렌더링함
    동적 라우팅 : 웹 애플리케이션에서 클라이언트의 요청에 따라 경로를 동적으로 처리하는 라우팅 방식
    사용자의 입력, 상태 변화, 다양한 조건에 따라 서버가 어떤 페이지나 리소스를 제공할지 결정
    경로변수 (Parameters)를 사용해 동적페이지를 만들 수 있음
    useParams → React Router에서 URL의 파라미터 값을 가져오는 Hook

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

  • UI/UX 디자인 전략
    느린 네트워크에서 사용자가 앱이 작동하지 않다고 느끼지 않도록 로딩상태를 명확하게 보여주어야 합니다.
    모든 콘텐츠를 한 번에 보여주려고 하면 초기 화면이 늦게 뜰 수 있어 사용자가 먼저 봐야 하는 정보를 우선으로 제공해야 합니다.
    데이터를 불러오는 동안 빈 화면이 보이면 오류라고 사용자가 생각할 수 있으므로 추가적인 UI를 제공하거나 화면 구조를 먼저 보여주며 사용자 경험을 개선합니다.
    느린 네트워크에서 요청 실패가 자주 발생할 수 있으므로 요청 실패에 대한 원인과 해결 방안을 제시하여 에러 상황을 안내하고 해결할 수 있도록 도와줍니다.

  • 기술적 최적화 방법
    이미지는 웹페이지에서 용량을 많이 차지하는 요소로 이미지 압축, 반응형 이미지 등 용량을 감소하며 품질을 해치지 않도록 하는 것이 중요합니다.
    디자인 전략과 동일하게 처음부터 모든 데이터를 불러오지 않고, 필요한 시점에 필요한 정보를 불러오는 방식을 활용합니다.
    캐싱을 사용하여 이미 불러온 데이터를 매번 다시 요청하지 않도록 저장해두는 방식을 사용합니다.
    필요한 데이터만 요청하거나 여러 API 호출을 묶는 등 요청 횟수를 줄여 네트워크가 느린 환경에도 작동하도록 할 수 있습니다.

→UI/UX 디자인 전략은 네트워크 속도가 느린 상황에서도 사용자가 불편함을 덜 느끼도록 하며 기술적 최적화 방법은 실제로 필요한 데이터를 더 적고, 더 빠르게 불러오도록 만드는 것이 중요합니다.

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

지역 상태 관리 : 특정 컴포넌트 또는 가까운 하위 컴포넌트에서만 필요한 상태
useState : 단순한 값이나 짧은 상태를 관리할 때 적합
→ 상태가 단순할 때, 업데이트 로직이 간단할 때, 한 컴포넌트 안에서만 사용할 때
useReducer : useState보다 상태 변경 로직이 복잡할 때 사용
→ 상태 값이 여러 개일 때, 상태 변경 로직이 복잡할 때, 이전 상태를 기반으로 변경할 때, 상태 변화 종류가 많을 때

Context API : 여러 컴포넌트가 같은 값을 공유할 수 있도록 함
일반적으로 부모에서 자식 컴포넌트로 props를 사용하여 데이터를 전달할 때 구조가 깊어지면 여러 단계를 거쳐 계속 내려야 하는데 Context API는 필요한 컴포넌트가 직접 값을 가져올 수 있게 함

전역 상태 관리 라이브러리 : 여러 컴포넌트, 여러 페이지에서 공유하는 상태를 더 체계적으로 관리하기 위한 도구
→ 여러 페이지에서 같은 상태를 공유, 상태 변경 로직이 복잡, 상태를 디버깅해야 할 때, 서버 데이터와 클라이언트 상태가 섞였을 때, 상태 변경 추적이 중요할 때
ex) Zustand를 사용하면 전역 store를 만들 수 있으며 컴포넌트 깊이에 상관없이 필요한 곳에서 상태를 가져올 수 있음

Copy link
Copy Markdown

@waldls waldls left a comment

Choose a reason for hiding this comment

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

4주차 과제 수고 많으셨습니다!
노션에서 디자이너님께 로컬스토리지 비우는 방법을 상세하게 설명해주신 점이 인상적이었어요 !!
다음 주 페어 과제도 열심히 해봅시다 🙌🏻

Comment thread public/favicon.svg
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

사용하지 않으시는 파일은 삭제해주셔도 좋을 것 같습니다!

<CurrentPlace />
<AlarmBox />

<main className="flex w-[var(--screen-width)] flex-col items-start gap-[12px] pt-[8px] overflow-y-auto">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

arbitrary value대신 Tailwind 기본 Spacing을 활용해보시면 좋을 것 같습니다!
gap-[12px] -> gap-3, pt-[8px] -> pt-2 처럼 4로 나눈 값으로 바로 사용할 수 있습니다!

Comment on lines +46 to +67
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 "";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기는 별도 utils로 분리하셔도 좋을 것 같습니다!

Comment on lines +59 to +79
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]";
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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];

Comment thread src/index.css
Comment on lines +18 to +23
/* typography */
--text-xs: 12px;
--text-sm: 14px;
--font-weight-regular: 400;
--font-weight-semibold: 600;
--line-height-tight: 140%;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image

현재 디자이너님께서 정의해주신 타이포그래피 스타일과 Pretendard 폰트가 정의되어있지 않은 것 같습니다!
아래처럼 정의해두고, 페이지나 컴포넌트에서 text-h1 으로 바로 사용할 수 있게 설정해두시면 좋을 것 같습니다!

@theme {
  --text-h1: 1.375rem;                
  --text-h1--line-height: 1.925rem;  
  --text-h1--letter-spacing: -0.03em;
  --text-h1--font-weight: 700;
}

Comment on lines +1 to +11
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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

절대 경로 설정을 하면 depth가 깊어질수록 가독성이 떨어지는 문제를 해결할 수 있을 것 같습니다!
설정 방법 관련 블로그 글 하나 남깁니닷 !!

Comment on lines +2 to +4

function PlaceHolder() {
return (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

해당 컴포넌트는 채팅 리스트 페이지에서 사람 또는 채팅방을 검색하는 컴포넌트이므로,
PlaceHolder 라는 컴포넌트명 대신 SearchInput 또는 ChatSearchInput으로 사용하시면 훨씬 직관적일 것 같습니다!

Comment on lines +31 to +35
<img
src={newChat}
alt="newChat"
className="w-[var(--size-24)] h-[var(--size-24)] shrink-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.

아이콘 SVG 파일에 SVGR 설정을 적용하면 <NewChat className="size-6 shrink-0" /> 형태로 간결하게 사용할 수 있을 것 같아요!

Copy link
Copy Markdown

@ryu-won ryu-won left a comment

Choose a reason for hiding this comment

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

고생많으셨어요!! 디자이너분과도 소통하시면서 열심히 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)]">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

button태그를 사용하셨는데, onclick핸들러가 없네요!

Comment thread src/App.tsx
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">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

스크린 사이즈가 하드코딩 되어있는듯합니다! 반응형 고려하셔서 min-h-dvh w-full max-w-[375px]
min-h-screen이 아니라 min-h-dvh로 해주시면 모바일에 더 최적화 될 것 같습니다! 하단 nav가 가려지는 경우가 종종있어서요!
h-screen 은 height: 100vh → 고정된 뷰포트 높이 100%
h-dvh 는 height: 100dvh → 동적으로 변하는 뷰포트 높이 100%라고 보시면 될듯합니다!

Image

dvh로 하시면 nav가 동적으로 위로 올라와서 보이게 됩니다!

참고 : https://abcdqbbq.tistory.com/104

};

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)]">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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>
      ))}

@a-00-a
Copy link
Copy Markdown

a-00-a commented Apr 27, 2026

오진님 4주차 과제도 수고많으셨습니다!

현재 배포링크를 실행했을 때 채팅방에서는 마지막 메시지가 localStorage에 정상적으로 반영되지만,
대화목록에서는 해당 변경이 반영되지 않는 문제가 있습니다.
localStorage는 상태 변경 시 자동으로 UI가 업데이트되지 않기 때문에,
채팅목록에서 해당 값을 다시 읽거나 상태를 갱신하는 로직이 필요해 보입니다!

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