[4주차] 이승연 과제 제출합니다.#18
Conversation
waldls
left a comment
There was a problem hiding this comment.
4주차 과제 수고하셨습니다!
현재 배포된 환경에서 특정 페이지 접속 후 새로고침을 하면 404 Not Found 에러가 발생하고 있습니다!
SPA 특성상 클라이언트 라우팅을 사용하기 때문에, Vercel이 모든 요청을 index.html로 보내주도록 하는 설정이 필요합니다. 프로젝트 루트에 vercel.json 파일을 추가하고 아래 설정을 적용해 주시면 해결될 것 같습니다!!
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}There was a problem hiding this comment.
현재 아이콘 파일명이 Frame 73.svg로 되어 있어, 이름만으로는 어떤 역할을 하는 아이콘인지 파악하기가 어려울 것 같습니다! (또한 이 파일은 public/images/ 경로에도 똑같이 들어있는 것 같습니다!)
그리고 프로젝트 내에 하이픈(-), 언더바(_), 대소문자가 혼용되고 있는데, 아이콘 네이밍 컨벤션을 하나로 통일해보는 건 어떨까요? 규칙이 일관되면 나중에 아이콘이 많아져도 검색이나 관리가 훨씬 수월해질 것 같습니다.
| <button type="button" onClick={handleCopy}> | ||
| <img src={copyIcon} alt="복사" className="h-5 w-5" /> | ||
| </button> |
There was a problem hiding this comment.
현재 프로젝트에 SVGR 설정이 되어 있는 것으로 알고 있습니다!
<img> 태그로 SVG를 불러오면 나중에 아이콘 색상을 변경하거나 스타일을 확장할 때 제약이 생길 수 있어요. 아래처럼 SVGR 컴포넌트 방식으로 교체하면 className이나 fill 등을 props로 직접 제어할 수 있어 훨씬 유연해질 것 같습니다.
import CopyIcon from '@/assets/icons/copy.svg?react'; // 경로 수정 필요
<button type="button" onClick={handleCopy}>
<CopyIcon className="size-5" aria-label="복사" />
</button>| const mockFriends: Friend[] = [ | ||
| { id: 1, name: '강감찬', status: 'read' }, | ||
| { id: 2, name: '김씨', status: 'yet' }, | ||
| { id: 3, name: '나훈아', status: 'none' }, | ||
| { id: 4, name: '남궁선', status: 'read' }, | ||
| { id: 4, name: '박지훈', status: 'none' }, | ||
| { id: 4, name: '백하린', status: 'yet' }, | ||
| { id: 4, name: '이지후', status: 'none' }, | ||
| { id: 4, name: '한다현', status: 'none' }, | ||
| { id: 4, name: '한아현', status: 'none' }, | ||
| ]; |
There was a problem hiding this comment.
mockData는 컴포넌트 파일 내에 두기보다는 src/mocks 혹은 별도의 constants 파일로 분리하는 건 어떨까요?!
로직과 데이터를 분리하면 컴포넌트 코드가 훨씬 간결해질 것 같습니다.
그리고 현재 데이터에 id: 4가 중복되어 있는데, 분리하면서 고유한 ID 값으로 수정해 주시면 더 좋을 것 같아요!
| {/*마지막 메시지*/} | ||
| <p className="max-w-[198px] h-4 Caption01R text-gray-60">{lastMessage?.messages ?? room.lastMessage}</p> |
| //전송 후 높이 초기화 | ||
| if (textareaRef.current) { | ||
| textareaRef.current.style.height = 'auto'; | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
메시지 전송 후에 입력창 높이가 자동으로 원래대로 돌아오도록 처리하신 부분이 인상 깊어요!
ux 측면에서 세심함이 느껴지는 것 같습니다 ㅎㅎ
ryu-won
left a comment
There was a problem hiding this comment.
추가 기능까지 구현하시고 부드러운 효과까지 나서 UI가 더 예쁘게 느껴졌던 것 같습니다! 시험기간이셨을텐데 고생많으셨어요~!
| onClick={() => navigate('/')} | ||
| className="flex h-8 w-8 items-center justify-center rounded-full transition-colors hover:bg-gray-30" | ||
| > | ||
| <img src={backIcon} alt="뒤로가기" className="h-8 w-8" /> |
| .Heading01R { | ||
| @apply text-[20px] font-normal tracking-[-1px] leading-[1.2]; | ||
| } | ||
| .Heading01SB { | ||
| @apply text-[20px] font-semibold tracking-[-1px] leading-[1.2]; | ||
| } |
There was a problem hiding this comment.
디자인 시스템 이름 자체를 컴포넌트 클래스로 등록하신점 잘하셨네요..!! 이러면 피그마보면서 바로바로 클래스로 넣을 수 있어서 장점인 것 같습니다:)
| const storedMessages = localStorage.getItem(MESSAGE_STORAGE_KEY); | ||
|
|
||
| if (storedMessages) { | ||
| return JSON.parse(storedMessages) as Message[]; |
There was a problem hiding this comment.
as 단언은 런타임 검증이 전혀 없어서 스토리지 데이터가 오염되면 런타임 오류가 발생될 수 있기에.
최소한 try-catch라도 넣으면 좋을 듯합니다!
const getInitialMessages = (): Message[] => {
try {
const stored = localStorage.getItem(MESSAGE_STORAGE_KEY);
if (!stored) return initialMessages;
return JSON.parse(stored) as Message[];
} catch {
return initialMessages; // 파싱 실패 시 초기값 fallback
}
};
| const getCurrentTime = () => { | ||
| const now = new Date(); | ||
| const hours = now.getHours(); | ||
| const minutes = String(now.getMinutes()).padStart(2, '0'); | ||
| const period = hours < 12 ? 'am' : 'pm'; | ||
| const displayHour = hours % 12 === 0 ? 12 : hours % 12; | ||
|
|
||
| return `${displayHour}:${minutes}${period}`; | ||
| }; | ||
|
|
||
| const getCurrentDate = () => { | ||
| const now = new Date(); | ||
| const year = now.getFullYear(); | ||
| const month = now.getMonth() + 1; | ||
| const date = now.getDate(); | ||
|
|
||
| return `${year}년 ${month}월 ${date}일`; | ||
| }; |
| "@/shared/*": ["shared/*"], | ||
| "@/app/*": ["app/*"], | ||
| "@/entities/*": ["entities/*"], | ||
| "@/features/*": ["features/*"], |

배포링크:
https://ceos-week3-react-messenger-23rd.vercel.app
피그마 링크:
Figma
QA노션 링크:
QA Notion
추가구현기능:
채팅목록 핀(Pin) 기능
-핀 고정: 이름 또는 멤버수 옆(투명도: 0인 상태) 을 클릭하면 리스트 맨 위에 고정됨
-핀 해제: 핀아이콘 누르기 -> 가장 아래쪽 핀 바로 아래로 이동함.
Review Questions
React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
: 동적 라우팅 = URL의 일부를 변수처럼 받아서, 같은 컴포넌트로 여러 페이지를 처리하는 방식입니다.
사용자 프로필, 게시글 수정 페이지, 블로그 상세 페이지, 상품 상세 페이지처럼 같은 화면 구조지만 데이터만 다른 페이지에 사용합니다.
네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
: 우선 사용자가 멈춘 화면이라고 느끼지 않게 로딩 상태를 보여주거나 전체 데이터를 다 기다리지 않고, 가능한 부분부터 보여줄 수 있습니다.
기술적 최적화로는 이미지 용량 줄이기, React Query와 같은 라이브러리를 사용하여 서버 데이터를 캐싱해 재사용하는 방법이 있습니다.
React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.
: useState는 모달 열림 여부, 입력값, 버튼 클릭 상태, 간단한 카운터와 같은 간단한 지역 상태에 적합합니다. 반면 useReducer는 장바구니와 같이 상태 변경 로직이 복잡할 때 사용합니다.
Context API는 여러 컴포넌트에 값 전달할 때 적합하고, 전역 상태 라이브러리는 크고 복잡한 앱의 전역 상태 관리에 적합합니다.