Skip to content
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

Open
wants to merge 70 commits into
base: main
Choose a base branch
from

Conversation

Shunamo
Copy link

@Shunamo Shunamo commented May 3, 2024

미션

배포 링크

배포

피그마링크

피그마

기능 구현

  1. 사용자 1명의 시점에서 여러명과 개인 대화를 한 형태로 대화 로직을 변경 (일대다)

  2. 대화 목록 화면, 친구 목록 화면, 마이페이지 화면, 통화 화면을 구현하고 라우팅함

  3. 대화 목록과 친구 목록에서의 검색 기능을 구현함
    대화 목록 → 친구 이름, 대화내용으로 검색
    친구 목록 → 친구 이름으로만 검색

  4. 대화 목록은 대화 내역이 있는 친구들만 랜더링됨.

  5. 대화목록 최신순으로 정렬

  6. 친구 목록에서 선택한 친구의 대화창으로 라우팅

  7. 대화 내역 O → 내역을 불러옴
    대화 내역 X → 친구의 프로필을 띄움

  8. 친구 목록, ChatAppHeader에서 전화 아이콘 기능
    → 전화 화면으로 라우팅
    → 선택한 user의 전화번호가 inputValue로 설정됨.
    전화번호는 표준형(010-0000-0000)을 따름.

  9. 기존의 꼬리물기 방식 수정
    분 단위 비교 → 그룹화

  10. 새로운 대화 버튼 기능 구현
    대화내역 없는 친구 목록 모달창
    → 모달에서 사용자 선택
    → 선택된 user와의 채팅방으로 라우팅

  11. 전화 연결 화면 구현
    전화번호 입력시 전화번호 prop으로 전달 후
    user.PhoneNumber과 비교해서
    저장된 전화번호가 있다면 user 명으로 출력

  12. 채팅방 삭제 기능 구현
    삭제하면 localstorage에서도 삭제되게!!

느낀점

저번 미션 때 대화창만 만들었고 대화 상대가 둘 뿐이라서 user id를 하드코딩해서 대화창을 구현했었는데.. 이걸 user 0 (로그인한 사용자)가 다대일로 바꿔서 구현하려고 하니까 잘 안됐습니다. 그 대화 안에서 프로필을 누르면 송수신자가 토글되는데, user 0을 고정 발신자라고 생각하고 해서 바로잡는데까지 오래걸렸습니다..ㅠㅠ
아직 타입스크립트를 사용하는게 어색하고 recoil도 잘 활용하지 못하는거 같아서 공부를 많이 해야겠다고 생각했습니다.

이 과정에서 처음 코드를 짤 때 애초에 라우팅이나 연동을 고려해서 예쁘게 짜놔야 됨을 깨달았습니다. 그리고 이미지 파일명도 귀찮아서 피그마에서 다운받은대로 했었는데 이렇게 하니까 나중에 수정할 일이 있을 때 너무 불편했습니다!

귀차니즘을 극복해야 멋지고 편하게 개발을 할 수 있는 것 같습니다...
이번 과제는 구현하는 화면이 여러개라서 더 재밌었습니다!

Key Questions

Routing이란?

라우팅은 여러 분야에서 쓰이는 용어지만 다 비슷한 의미를 가지고 있는 것 같다.
React에서 다루는 routing이란, 사용자가 다양한 url을 통해서 App의 다른 페이지나 component 로 이동할 수 있게 해주는 과정이다.

리액트에서는 라우팅 기능이 없어서 React Router 라이브러리를 사용한다.

React Router을 사용해서 경로를 설정하는 방식에 대해 알아봤다.

  1. path란?
이런식으로 path를 작성하는데, 이 path는 url과 일치시킬 경로 패턴이다.
  1. Dynamic Segment란?
path segment가 :으로 시작하면 동적 세그먼트가된다. path가 url과 일치하면 segment가 url에서 파싱되고 params로 다른 router API들에 전달된다. 하나의 경로 경로에 여러 동적 세그먼트가 있을 수 있습니다. ;
  1. Optional Segment란?
segment의 끝에 ?를 붙이면된다.
  1. Splats란?
경로 패턴이 /*로 끝나면 다른 문자를 포함해서 / 뒤에 오는 모든 문자와 일치함

를 사용하기만 해도 페이지 간에 링크를 만들 수 있지만, 사용자가 링크를 클릭할 때마다 새로운 페이지로 '전체' 로드가 발생한다. 이러면 웹 페이지의 로딩 속도를 늦추고, 리소스 낭비가 심해지고, 사용자 경험 측면에서도 완전 안좋다.

하지만 SPA 방식을 사용하면 이 문제를 해결할 수 있다.

SPA란?

SPA: Single Page Application
단일 페이지 애플리케이션 (Single-page application, SPA)은 단일 웹 문서만 로드한 다음 다른 콘텐츠가 표시될 때 XMLHttpRequest 및 Fetch와 같은 JavaScript API를 통해 해당 단일 문서의 본문 콘텐츠를 업데이트하는 웹 앱 구현체.

초기 로드 시 애플리케이션에 필요한 모든 HTML, JavaScript, CSS가 한 번에 로드되며, 이후의 사용자 상호작용은 이 페이지 내에서 AJAX와 HTML5 API를 통해 동적으로 처리됨.

장점: 사용자는 서버에서 완전히 새로운 페이지를 로드하지 않고도 웹사이트를 사용할 수 있으므로, 성능이 향상되고 보다 동적인 경험을 얻을 수 있다.

단점: SEO와 같은 일부 절충이 되는 단점이 있음

즉 routing을 통해서 spa의 핵심 기능을 구현할 수 있다. 페이지 전체를 리로딩하는게 아니라 필요한 부분이나 데이터만 동적으로 갱신해주기 때문

상태관리란?

애플리케이션의 다양한 부분(사용자 인터페이스 상태, 사용자 입력, 서버 응답 등)에서 필요한 데이터의 흐름과 변화를 중앙에서 조정하고 관리하는 과정.

React에서의 상태 관리 방법

  1. React 내장 Hook (useState, useEffect 등등)
  • Hook 규칙
    1. 최상위(at the Top Level)에서만 Hook을 호출해야함
      반복문, 조건문 혹은 중첩된 함수 내에서 Hook 호출 X

    2. 오직 React 함수 내에서만 Hook을 호출

    3. 모든 렌더링에서 Hook의 호출 순서는 같아서
      React는 Hook이 호출되는 순서에 의존한다.

  1. Recoil

Recoilatom을 통해 selectors를 거쳐 React 컴포넌트로 흐르는 데이터 흐름 그래프를 생성할 수 있다.

  1. atom: 컴포넌트가 구독할 수 있는 상태의 단위
    이렇게 생성됨
const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

Atom에는 디버깅, 지속성 및 모든 Atom의 맵을 볼 수 있는 특정 고급 API에 사용되는 고유 key가 필요함. React 구성 요소 상태와 마찬가지로 default도 있음

구성 요소에서 원자를 읽고 쓰려면 useRecoilState 이라는 훅을 사용함. React의 useState와 비슷하지만 atom은 여러 컴포넌트에서 공유될 수 있는 전역 상태임

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

버튼을 클릭하면 버튼의 글꼴 크기가 1씩 늘어나는데 다른 구성 요소도 동일한 글꼴 크기를 사용할 수 있음.

  1. Selectors: 파생된 상태 또는 상태 기반의 계산을 정의하는데 사용 atom의 값을 입력으로 받아 계산을 수행하고, 그 결과를 반환함

최소 상태 세트가 atom에 저장되고 다른 모든 것은 해당 최소 상태의 함수로 효율적으로 계산되므로 중복 상태를 피할 수 있음

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => { //get: 계산할 함수
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

useRecoilValue() : 읽기 전용
-> 상태를 변경할 필요가 없고 오직 읽기만 필요한 경우 사용
useRecoilState() : 읽고 쓸 수 있음
-> 상태를 읽고 변경해야 하는 경우 사용

dreamforxou added 30 commits March 25, 2024 03:44
2. 헤더 구현
3. 메인컨테이너 구현
2. 메세지 로컬 스토리지에 저장 기능 구현
feat : 메세지 전송시 스크롤바 하단으로 자동 이동
2. atom으로 메세지, 사용자, 상대방 상태 관리하기
3. 토글 기능 구현
4. 수신자 발신자에 따른 조건부 랜더링
2. style : framer 라이브러리로 애니메이션 추가
3. chore : fakedata json만 남겨두고 임의로 만들었던 친구목록 지우기
dreamforxou and others added 28 commits May 3, 2024 16:03
2. 메세지 꼬리물기 분 단위로 바꿈
Copy link

@noeyeyh noeyeyh left a 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 = () => {
Copy link

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를 사용하는 것은 안티 패턴이라고 합니다..!

리액트에서 FC를 사용하지 말아야하는 이유

height: 35px;

transition: width 0.3s ease;
`;
Copy link

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

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>
Copy link

Choose a reason for hiding this comment

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

저는 AM, PM 형식으로 바꿀 때 따로 함수를 작성했는데
수현님처럼 별도의 함수 없이 작성하는 방법은 더 간단해보여요! 배워갑니다 👍

});

return grouped;
};
Copy link

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

Choose a reason for hiding this comment

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

대소문자 구분하지 않고 검색하는 것도 섬세한 것 같아요!


const closeModal = () => {
setModalOpen(false);
};
Copy link

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;
`;
Copy link

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 속성을 사용하면 반응형에서도 더 유용하게 쓸 수 있을 것 같아요! 👍

Copy link

@jinnyleeis jinnyleeis left a 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) {

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

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());

Choose a reason for hiding this comment

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

Map 객체의 유일성이란 특성을 중복 메시지 제거하는데 활용하신점이 대단하신 것 같아요!

Comment on lines +20 to +35

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


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 상태 업데이트

Choose a reason for hiding this comment

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

setSelectedUserId를 사용하여 선택된 사용자의 상태를 업데이트하는 게 useRecoilState를 잘 활용하신 것 같네요 recoil을 잘 활용하고 계신것 같아 많이 배워가요

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