-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[4주차] 김수현 미션 제출합니다. #19
base: main
Are you sure you want to change the base?
Conversation
2. 헤더 구현 3. 메인컨테이너 구현
2. 메세지 로컬 스토리지에 저장 기능 구현
2. 전체적인 화면 구조 변경
feat : 메세지 전송시 스크롤바 하단으로 자동 이동
2. atom으로 메세지, 사용자, 상대방 상태 관리하기 3. 토글 기능 구현 4. 수신자 발신자에 따른 조건부 랜더링
2. style : framer 라이브러리로 애니메이션 추가 3. chore : fakedata json만 남겨두고 임의로 만들었던 친구목록 지우기
2. 메세지 꼬리물기 분 단위로 바꿈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 수현님! 다양한 기능과 여러 페이지 구현하셔서 재밌게 잘 봤어요.!!
애니메이션 효과도 다 적용해주시고 항상 CSS에 진심이시구,, 정말 섬세하신 것 같아요. 👍👍
주석도 쉽게 잘 작성해주셔서 코드 이해하는 데 도움이 되었어요. 많이 배우고 갑니다.!!
|
||
|
||
|
||
const App: React.FC = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React.FC
를 사용하면 children의 타입을 일일이 작성하지 않아도 되지만,
리액트 17 이하 버전에서 props는 type이 ReactNode인 children을 암시적으로 가지고 있어
React.FC
를 사용하는 것은 안티 패턴이라고 합니다..!
height: 35px; | ||
|
||
transition: width 0.3s ease; | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클릭 시 input 창의 크기가 변하는 것과 부드럽게 전환되는 애니메이션 효과를 추가해주셨네요! 👍
useEffect(() => { | ||
console.log("Selected User ID:", selectedUserId); | ||
console.log("Filtered Messages:", filteredMessages); | ||
}, [selectedUserId, filteredMessages]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
보통 커밋할 때 console.log
는 삭제하고 하는 것이 일반적이라구 합니다..!
아니면 혹시 수현님의 다른 깊은 뜻이 있을까요..?!
<Timestamp isSender={message.senderId === selectedUserId}> | ||
{new Date(message.timestamp).toLocaleTimeString('ko-KR', | ||
{ hour: 'numeric', minute: '2-digit', hour12: true }).replace('AM', '오전').replace('PM', '오후')} {/*오전 12:26 형식으로 '시간' 출력하기!!!*/} | ||
</Timestamp> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 AM, PM 형식으로 바꿀 때 따로 함수를 작성했는데
수현님처럼 별도의 함수 없이 작성하는 방법은 더 간단해보여요! 배워갑니다 👍
}); | ||
|
||
return grouped; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 매번 채팅마다 시간을 출력해야 해서 분이 달라질 경우를 나눌 필요가 없었는데요..!
날짜 기준으로 문자열을 그룹화하고 같은 날짜 안에서도 연속적인 메시지를 1분 이내인지 확인하고 그룹을 나누는 방식으로 구현하신 거 보고 배워가요. 코드 깔끔하게 작성해주셔서 이해하기 쉬운 것 같아요 👍
user.name.toLowerCase().includes(searchTerm.toLowerCase()) || | ||
messages.some(msg => | ||
(msg.senderId === user.id || msg.receiverId === user.id) && | ||
msg.text.toLowerCase().includes(searchTerm.toLowerCase()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
대소문자 구분하지 않고 검색하는 것도 섬세한 것 같아요!
|
||
const closeModal = () => { | ||
setModalOpen(false); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleNewChatClick 함수와 closeModal 함수를 하나로 통합하여 모달의 열고 닫는 데 사용해서 setModalOpen
의 상태를 전환해도 좋을 것 같아요!
width: 100%; | ||
align-items: center; | ||
justify-content: center; | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 이런 식으로 몇 개의 열을 출력해야 할 때 map함수에서 여러 열을 생성하도록 했는데
수현님 grid-template-columns: repeat(3, 1fr);
코드처럼 grid 속성을 사용하면 반응형에서도 더 유용하게 쓸 수 있을 것 같아요! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수현님 라우팅에 애니메이션 라이브러리를 통해 전환 효과를 구현하신게 우선 너무 인상깊습니다..!
framer-motion 처음 알게 되었는데 저도 활용해봐야겠네요..!
항상 추가적인 기능들도 멋있게 구현해주시는 것 같아 많이 배워가요!!
과제 수고 많으셨습니다 ㅎㅎ
|
||
const handleButtonClick = (value: string) => { | ||
setInputValue(prev => { | ||
if (prev.replace(/-/g, '').length === 3 || prev.replace(/-/g, '').length === 7) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오...
추가 기능으로 전화번호 처리 로직을 구현하셨는데,
여기에서 정규 표현식까지 사용하여 데이터를 가공하신게 정말 인상깊습니다
const messagesString = localStorage.getItem('messages'); | ||
const messagesFromLocalStorage = messagesString ? JSON.parse(messagesString) : []; | ||
//처음 가짜데티어랑 내가 추가로 입력한거랑 합침 | ||
const combinedMessages = [...messagesData, ...messagesFromLocalStorage]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로컬 스토리지와 정적 데이터를 효과적으로 결합하여 초기 상태를 구성해 데이터의 일관성있게 관리할 수 있는 점이 인상깊어요!
const combinedMessages = [...messagesData, ...messagesFromLocalStorage]; | ||
|
||
// id기준으로 중복 메세지 제거 | ||
const uniqueMessages = Array.from(new Map(combinedMessages.map(msg => [msg.id, msg])).values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map 객체의 유일성이란 특성을 중복 메시지 제거하는데 활용하신점이 대단하신 것 같아요!
|
||
return ( | ||
<AnimatePresence mode='wait'> | ||
<Routes location={location} key={location.pathname}> | ||
<Route path="/" element={<ChatList />} /> | ||
<Route path="/chat/:userId" element={<ChatApp />} /> | ||
<Route path="/friends" element={<FriendList />} /> | ||
<Route path="/mypage" element={<MyPage/>}/> | ||
<Route path="/phone" element={<PhoneCall/>}/> | ||
<Route path="/active-call" element={ <ActiveCall phoneNumber={phoneNumber} />} /> | ||
</Routes> | ||
</AnimatePresence> | ||
); | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React Router와 Framer Motion을 결합하여 애니메이션 효과를 포함한 라우팅을 구현하신게 정말 사용자 경험면에 있어서 좋은 것 같아요.
AnimatePresence 컴포넌트로 페이지를 전환할 때마다 매끄러운 애니메이션 처리해주신게 인상깊습니다.
라우팅을 정말 잘 구현하신 것 같아요..!
}; | ||
|
||
const handleUserClick = (userId: number) => { | ||
setSelectedUserId(userId); // 사용자 클릭시 selectedUserId 상태 업데이트 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setSelectedUserId를 사용하여 선택된 사용자의 상태를 업데이트하는 게 useRecoilState를 잘 활용하신 것 같네요 recoil을 잘 활용하고 계신것 같아 많이 배워가요
미션
배포 링크
배포
피그마링크
피그마
기능 구현
사용자 1명의 시점에서 여러명과 개인 대화를 한 형태로 대화 로직을 변경 (일대다)
대화 목록 화면, 친구 목록 화면, 마이페이지 화면, 통화 화면을 구현하고 라우팅함
대화 목록과 친구 목록에서의 검색 기능을 구현함
대화 목록 → 친구 이름, 대화내용으로 검색
친구 목록 → 친구 이름으로만 검색
대화 목록은 대화 내역이 있는 친구들만 랜더링됨.
대화목록 최신순으로 정렬
친구 목록에서 선택한 친구의 대화창으로 라우팅
대화 내역 O → 내역을 불러옴
대화 내역 X → 친구의 프로필을 띄움
친구 목록, ChatAppHeader에서 전화 아이콘 기능
→ 전화 화면으로 라우팅
→ 선택한 user의 전화번호가 inputValue로 설정됨.
전화번호는 표준형(010-0000-0000)을 따름.
기존의 꼬리물기 방식 수정
분 단위 비교 → 그룹화
새로운 대화 버튼 기능 구현
대화내역 없는 친구 목록 모달창
→ 모달에서 사용자 선택
→ 선택된 user와의 채팅방으로 라우팅
전화 연결 화면 구현
전화번호 입력시 전화번호 prop으로 전달 후
user.PhoneNumber과 비교해서
저장된 전화번호가 있다면 user 명으로 출력
채팅방 삭제 기능 구현
삭제하면 localstorage에서도 삭제되게!!
느낀점
저번 미션 때 대화창만 만들었고 대화 상대가 둘 뿐이라서 user id를 하드코딩해서 대화창을 구현했었는데.. 이걸 user 0 (로그인한 사용자)가 다대일로 바꿔서 구현하려고 하니까 잘 안됐습니다. 그 대화 안에서 프로필을 누르면 송수신자가 토글되는데, user 0을 고정 발신자라고 생각하고 해서 바로잡는데까지 오래걸렸습니다..ㅠㅠ
아직 타입스크립트를 사용하는게 어색하고 recoil도 잘 활용하지 못하는거 같아서 공부를 많이 해야겠다고 생각했습니다.
이 과정에서 처음 코드를 짤 때 애초에 라우팅이나 연동을 고려해서 예쁘게 짜놔야 됨을 깨달았습니다. 그리고 이미지 파일명도 귀찮아서 피그마에서 다운받은대로 했었는데 이렇게 하니까 나중에 수정할 일이 있을 때 너무 불편했습니다!
귀차니즘을 극복해야 멋지고 편하게 개발을 할 수 있는 것 같습니다...
이번 과제는 구현하는 화면이 여러개라서 더 재밌었습니다!
Key Questions
Routing이란?
라우팅은 여러 분야에서 쓰이는 용어지만 다 비슷한 의미를 가지고 있는 것 같다.
React에서 다루는 routing이란, 사용자가 다양한 url을 통해서 App의 다른 페이지나 component 로 이동할 수 있게 해주는 과정이다.
리액트에서는 라우팅 기능이 없어서 React Router 라이브러리를 사용한다.
React Router을 사용해서 경로를 설정하는 방식에 대해 알아봤다.
를 사용하기만 해도 페이지 간에 링크를 만들 수 있지만, 사용자가 링크를 클릭할 때마다 새로운 페이지로 '전체' 로드가 발생한다. 이러면 웹 페이지의 로딩 속도를 늦추고, 리소스 낭비가 심해지고, 사용자 경험 측면에서도 완전 안좋다.
하지만 SPA 방식을 사용하면 이 문제를 해결할 수 있다.
SPA란?
SPA: Single Page Application
단일 페이지 애플리케이션 (Single-page application, SPA)은 단일 웹 문서만 로드한 다음 다른 콘텐츠가 표시될 때 XMLHttpRequest 및 Fetch와 같은 JavaScript API를 통해 해당 단일 문서의 본문 콘텐츠를 업데이트하는 웹 앱 구현체.
초기 로드 시 애플리케이션에 필요한 모든 HTML, JavaScript, CSS가 한 번에 로드되며, 이후의 사용자 상호작용은 이 페이지 내에서 AJAX와 HTML5 API를 통해 동적으로 처리됨.
장점: 사용자는 서버에서 완전히 새로운 페이지를 로드하지 않고도 웹사이트를 사용할 수 있으므로, 성능이 향상되고 보다 동적인 경험을 얻을 수 있다.
단점: SEO와 같은 일부 절충이 되는 단점이 있음
즉 routing을 통해서 spa의 핵심 기능을 구현할 수 있다. 페이지 전체를 리로딩하는게 아니라 필요한 부분이나 데이터만 동적으로 갱신해주기 때문
상태관리란?
애플리케이션의 다양한 부분(사용자 인터페이스 상태, 사용자 입력, 서버 응답 등)에서 필요한 데이터의 흐름과 변화를 중앙에서 조정하고 관리하는 과정.
React에서의 상태 관리 방법
최상위(at the Top Level)에서만 Hook을 호출해야함
반복문, 조건문 혹은 중첩된 함수 내에서 Hook 호출 X
오직 React 함수 내에서만 Hook을 호출
모든 렌더링에서 Hook의 호출 순서는 같아서
React는 Hook이 호출되는 순서에 의존한다.
Recoil
은atom
을 통해selectors
를 거쳐React
컴포넌트로 흐르는 데이터 흐름 그래프를 생성할 수 있다.atom
: 컴포넌트가 구독할 수 있는 상태의 단위이렇게 생성됨
Atom에는 디버깅, 지속성 및 모든 Atom의 맵을 볼 수 있는 특정 고급 API에 사용되는 고유 key가 필요함. React 구성 요소 상태와 마찬가지로 default도 있음
구성 요소에서 원자를 읽고 쓰려면 useRecoilState 이라는 훅을 사용함. React의 useState와 비슷하지만 atom은 여러 컴포넌트에서 공유될 수 있는 전역 상태임
버튼을 클릭하면 버튼의 글꼴 크기가 1씩 늘어나는데 다른 구성 요소도 동일한 글꼴 크기를 사용할 수 있음.
Selectors
: 파생된 상태 또는 상태 기반의 계산을 정의하는데 사용atom
의 값을 입력으로 받아 계산을 수행하고, 그 결과를 반환함최소 상태 세트가
atom
에 저장되고 다른 모든 것은 해당 최소 상태의 함수로 효율적으로 계산되므로 중복 상태를 피할 수 있음useRecoilValue()
: 읽기 전용-> 상태를 변경할 필요가 없고 오직 읽기만 필요한 경우 사용
useRecoilState()
: 읽고 쓸 수 있음-> 상태를 읽고 변경해야 하는 경우 사용