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주차] 이가빈 과제 제출합니다. #13

Open
wants to merge 54 commits into
base: master
Choose a base branch
from

Conversation

billy0904
Copy link

배우고 느낀 점

  • 이번 4주차 과제를 통해 전역 상태 관리의 중요성을 깊이 느꼈습니다. 특히 여러 컴포넌트에서 공유되는 데이터를 실시간으로 동기화되도록 (ex. 안 읽은 메시지 개수를 채팅방 목록에서도 사용하고 네비게이션바에서도 사용하는 것 등) 하는 것이 심도 있는 서비스 구현에 필수적이라는 생각이 들었습니다.
  • 전역 상태 관리 구현을 위해 Context API와 Redux의 차이를 찾아봤었는데 Redux가 Context API보다 전역 상태 관리 이외의 기능들을 더 많이 제공하고, Context API가 high-frequency한 기능에 제한적이라는 것을 알게 되었습니다. 이번 과제의 볼륨이나 사용성 등을 고려해보았을 때 크게 복잡한 상태 관리가 필요한 것이 아니었고, 다른 프로젝트들에서 라이브러리를 사용하는 경우는 많이 있었지만 Context API로 전역 상태 관리를 하는 경우를 많이 보지 못했던 것 같아 다른 전역 상태 관리 라이브러리를 사용하는 대신 Context API를 선택하였습니다. 이후에 더 규모있고 활발하게 사용되는 서비스를 구현하게 된다면 다른 상태 관리 라이브러리들도 사용해보고 싶습니다.

많은 시간을 투자한 부분

  • 지난 과제에서는 토글로 사용자 변경을 구현했기 때문에 사용자가 두 명일 경우만을 상정하고 로직을 구상했는데 이번에는 사용자가 여러 명일 경우를 고려해야 해서 채팅 채널 동적 생성과 사용자 전환 기능 구현에 많은 시간을 투자한 것 같습니다. 원래 구현했던 로직을 뜯어고치는 과정이 생각보다 번거로웠고 특히 채팅창 라우팅 URL을 chat/:userId로 설정했더니 사용자가 전환되었을 때 메시지가 렌더링되지 않거나 섞이는 등의 오류가 있어서 이를 chatKey 를 부여하는 방식으로 해결하는데 시간이 오래 걸렸습니다.
  • 특히, 네비게이션 바와 각 채팅방 리스트에 읽지 않은 메시지 수를 표시해야 해서 메시지 읽음 기능을 구현하고 확인하기 위해 사용자 시점을 두 가지(이가빈, 김태양)로 설정하였습니다. 지난 주차 과제에서 구현한 사용자를 토글하는 방식을 유지하면서 메신저를 만들었는데 그 과정에서 각 채널별 채팅방에서 나눈 메시지를 로컬 스토리지에 분리해서 저장하는 방식을 구현하는 것이 어려웠던 것 같습니다.

구현 기능

  • (지난 과제 추가 기능) 상대방 메시지에 반응 남기기
  • React Router를 사용하여 친구 목록, 채팅방 목록, 채팅방 등의 페이지 구성
  • 로컬스토리지에 채팅방 내용 및 상태 관리 내용 저장
  • 메세지에 유저 정보 표시
  • 멀티프로필 버튼 클릭 시 사용자 전환
    • 내 프로필 화면에서 멀티프로필 버튼 클릭 시 똑같이 사용자 전환 가능
    • 이가빈/김태양 두 사용자 간 전환만 가능
    • 나머지 뉴진스 멤버들로는 전환 불가 (메시지 보내기만 가능)
  • 친구 목록에서 친구 클릭 시 그 친구와의 채팅창으로 이동
  • 오픈채팅, 쇼핑, 더보기 페이지는 "서비스 준비중" 안내문구 표시
  • 이가빈-김태양 대화창에만 디폴트 대화 有
  • 안 읽은 메시지 개수 표시 기능 (채팅창 + 네비게이션바 채팅 버튼)

배포 링크

https://react-messenger-20th-sable.vercel.app/

Key Question

1. React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?

// 동적 라우팅
<Route path='/chat/:chatKey' element={<ChatRoomPage />}></Route>

// 정적 라우팅
<Route path='/chatlist' element={<ChatRoomListPage />}></Route>

✅정적 라우팅

라우터 컴포넌트에서 경로와 보여줄 컴포넌트를 미리 전부 정의해두는 방식

  • 라우팅을 설정하는 가장 기본적인 방식
  • 큰 규모의 복잡한 어플리케이션은 경로를 미리 설정하는 라우팅 방식으로는 한계가 있다.
    • ex. 사용자별 채팅방 생성 등

✅동적 라우팅

url 전체 형태를 미리 정의하지 않고 규칙을 정의하여 경로를 표현하는 방식

  • path prop에 : 기호를 사용하여 path/:문자열 의 형태로 표현한다.
  • 경로/ 뒤에 무슨 문자열이 오든 해당 Route로 연결된다.
  • 위 예시 코드의 chatKey와 같은 문자열을 path parameter라고 한다.
  • path parameter 는 url 에 있는 값을 매개변수처럼 사용하는 것으로 동일한 큰 틀 내에서 다른 UI 요소들을 렌더링하도록 할 수 있다.
  • 동적 라우팅을 사용하면 규칙을 만족하는 모든 URL을 상세 페이지로 연결시킬 수 있기 때문에 확장성을 높일 수 있다.

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

✅UI/UX 디자인 전략

  • Skeleton UI (스켈레톤 로딩)
    • 콘텐츠 로딩 중 화면에 빈 상자를 표시하여 화면이 로딩 중임을 명확하게 시각적으로 알려주어 로딩이 끝나기 전까지 시각적인 틀을 먼저 제공하므로 사용자는 느린 환경에서도 기다리는 답답함을 덜 수 있다.
  • 미리 보기 이미지 제공
    • 이미지 로딩이 오래 걸릴 경우 저해상도 미리 보기 이미지를 먼저 보여주고난 후 이후 고해상도 이미지로 전환하는 방식을 사용할 수 있다.
  • Lazy Loading (지연 로딩)
    • 동영상 등의 무거운 콘텐츠는 사용자가 화면을 스크롤한 후 해당 콘텐츠가 사용자에게 노출되었을 때 로드하는 방식을 적용하여 불필요한 초기 로드를 줄이고 초기 화면 로딩 시간을 단축시킬 수 있다.
  • 로딩 인디케이터와 피드백 제공
    • 로딩 중에는 스피너나 진행 바를 통해 사용자에게 피드백을 제공하여 화면이 정지된 것처럼 보이지 않게 구성한다.
  • 오프라인 상태 지원
    • 오프라인 상태에서 사용 가능한 기능을 제공하거나 저장 가능한 콘텐츠를 표시하는 등 네트워크 상태에 따라 앱 동작을 최적화한다.

✅기술적 최적화 방법

  • 콘텐츠 압축
    • 이미지, 동영상 등의 콘텐츠는 최적화된 형식을 사용하거나 압축하여 데이터 전송량을 줄인다.
  • Code Splitting과 지연 로드
    • React와 같은 SPA에서는 코드 분할을 통해 필요할 때 필요한 코드만 로드하도록 하여 초기 로딩 시간을 단축할 수 있다.
  • HTTP 요청 최적화
    • 리소스를 병합하거나 HTTP/2를 사용해 요청 수를 줄이고, 캐싱을 통해 재요청을 최소화한다.
    • 캐싱 전략의 경우 구체적으로 세분화하여 사용자가 같은 리소스를 반복해서 요청하지 않도록 설정한다.
  • 프리페치(Pre-Fetch) / 프리로드(Pre-Load)
    • 사용자가 곧 필요로할 가능성이 높은 리소스를 미리 로드해두어 실제로 필요할 때 빠르게 제공될 수 있도록 한다.

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

✅지역 상태

특정 컴포넌트 안에서만 관리되는 상태

  • 다른 컴포넌트들과 데이터를 공유하지 않는다.

useState

컴포넌트 내부의 단순한 상태관리에 사용

  • 상태가 하나 또는 두 개의 값으로 이루어진 경우, 또는 상태 변화가 복잡하지 않은 경우에 적합하다.
  • 상태와 상태 변경 함수를 제공하며 상태를 업데이트하면 컴포넌트가 리렌더링된다.
  • 직관적이고 코드가 간단하며 로컬 상태를 빠르게 업데이트하는데 효과적이다.
  • 상태 관리 로직이 복잡해지거나 상태가 여러 컴포넌트에 걸쳐 공유될 필요가 있는 경우에는 적합하지 않다.

useReducer

복잡한 상태 로직이 필요한 경우에 사용

  • ex. 여러 개의 상태를 한 번에 업데이트하거나 상태 변경에 조건이 있을 때.
  • 액션과 리듀서를 통해 상태를 업데이트하므로 상태 관리가 구조화되고 가독성이 높아진다.
  • 리듀서 함수에 의해 상태가 업데이트되기 때문에 로직이 복잡해도 코드가 깔끔하다.
  • useState와 달리 다양한 액션에 따라 다양한 상태 변화를 관리하기 용이하다.
  • 단순한 상태에 사용하기에는 오히려 코드가 복잡해질 수 있기 때문에 상태가 여러 컴포넌트에 걸쳐 공유되어야 하는 경우에는 적합하지 않다.

✅전역 상태 관리

프로젝트 전체에 영향을 끼치는 상태

  • 하나의 상태 변경이 다른 여러 컴포넌트에 영향을 주는 경우가 많아 전역 상태 관리가 필요하다.

Context API

특정 상태를 어플리케이션의 여러 컴포넌트에서 공유해야 할 때 사용

  • ex. 인증 상태, 테마 설정, 언어 설정 등 전역에서 필요한 정보가 있을 경우
  • React.createContext()를 통해 상태를 전역으로 관리할 수 있다.
  • Provider를 통해 데이터를 하위 컴포넌트에 전달하며, useContext 훅을 통해 데이터에 접근한다.
  • 작은 규모의 전역 상태 공유에 적합하고 설정이 간단하다.
  • 전역 상태를 제공할 위치와 하위 트리에서 구독할 위치를 쉽게 정의할 수 있다.
  • 상태값 변경 시 Provider 하위에 있는 모든 Consumer 컴포넌트를 리렌더링하므로 컴포넌트 트리가 깊거나 구독하는 컴포넌트가 많아질수록 불필요한 리렌더링이 발생한다.
  • 여러 상태를 각기 다른 Context로 관리해야 할 경우 구조가 복잡해질 수 있다.

전역 상태 관리 라이브러리

대규모 어플리케이션에서 복잡한 상태를 일관되게 관리할 때 사용

  • Redux, Recoil, MobX, Zustand 등이 있다.
  • 상태가 여러 컴포넌트와 모듈에 걸쳐 얽혀있거나 비동기 로직이 포함된 경우에 유용하게 사용된다.
  • 상태 관리 로직이 컴포넌트에서 분리되어 있어 상태의 흐름이 명확하고 미들웨어(Redux Thunk, Redux Saga 등)를 통해 비동기 작업을 관리할 수 있다.
  • 디버깅이나 상태 추적이 용이하다.
    • 특히 상태 트리가 복잡하거나 특정 상태를 여러 컴포넌트에서 조작할 필요가 있을 때 매우 효과적이다.
  • 초기 설정과 코드 구조가 복잡하기 때문에 어플리케이션 볼륨이 작은 경우에는 적절하지 않다.

Copy link

@psst54 psst54 left a comment

Choose a reason for hiding this comment

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

과제 너무 고생하셨습니다!!❤️‍🔥❤️‍🔥❤️‍🔥

Comment on lines +2 to +4
import profileIcon from "../../assets/ChatRoom/profile.svg";
import { UserData } from '../../lib/UserData';
import { formatTimeForChatList } from '../../utils/ClockUtils';
Copy link

Choose a reason for hiding this comment

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

가빈님도 절대경로를 설정해보는건 어떨까용?! import가 훨씬 쉬워질거에요!

Copy link

Choose a reason for hiding this comment

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

다른 파일들과 함께 보니 전반적으로 따옴표와 쌍따옴표를 혼용하고 계신 것 같아요. 이런 부분은 Prettier를 쓰면 쉽게 통일할 수 있습니다!

return (
<div
className="w-full h-chatBarHeight bg-White flex items-center pl-[16px]"
style={{ boxShadow: "0px -4px 8px 0px rgba(0, 0, 0, 0.08)" }}
Copy link

Choose a reason for hiding this comment

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

앗 이부분은 tailwind로 처리하지 않으신 이유가 있을까요?

Copy link

Choose a reason for hiding this comment

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

이렇게 indent depth가 깊어지면 나중에 원하는 부분을 찾기 어려워질 것 같아요. 컴포넌트로 분리할 수 있는 부분은 분리해보는게 어떨까요?

if (!currentUser) return;

// chatKey 생성
const chatKey = `${Math.min(currentUser.userId, userId)}_${Math.max(currentUser.userId, userId)}`;
Copy link

Choose a reason for hiding this comment

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

오 key를 생성하는 방식이 특이하네요!👍

Copy link

Choose a reason for hiding this comment

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

헉 펑까지 구현하셨네요😂😂😂

// 각 유저별 메시지 불러오기
if (currentUser && user.userId !== currentUser.userId) {
// currentUser와 user 간의 공통 대화 키 생성
const chatKey = `messages_${Math.min(currentUser.userId, user.userId)}_${Math.max(currentUser.userId, user.userId)}`;
Copy link

Choose a reason for hiding this comment

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

key를 구하는 함수는 따로 빼서 재사용해도 될 것 같아요!

<Line />
<Birthday />
<Line />
<FriendList />
Copy link

Choose a reason for hiding this comment

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

컴포넌트를 나눠서 깔끔하고 좋네용ㅎㅎ👍

Comment on lines +27 to +39
if (diffInDays === 0) {
return timeString;
} else if (diffInDays === 1) {
return `(어제) ${timeString}`;
} else if (diffInDays === 2) {
return `(그저께) ${timeString}`;
} else if (diffInDays <= 3) {
return `(${diffInDays}일 전) ${timeString}`;
} else {
const month = date.getMonth() + 1;
const day = date.getDate();
return `(${month}/${day}) ${timeString}`;
}
Copy link

Choose a reason for hiding this comment

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

자세한 분기 처리 좋아용👍👍
그런데 if문 안에 return이 있기 때문에 뒤에서는 else if가 아니라 그냥 If로, else는 없애도 된답니다!
이 부분도 함수로 따로 빼서 재사용하셔도 좋을 것 같아요~

type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}

Choose a reason for hiding this comment

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

image

사진처럼 onKeyDown 썼을 때 한글 마지막이 두번 입력 되는데 한번 읽어보세요

Comment on lines +110 to +111
{/* 상대방 메시지 렌더링 */}
{!isCurrentUser && (

Choose a reason for hiding this comment

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

메세지 부분 컴포넌트로 분리해보는 건 어떨까요?

Copy link

@hiwon-lee hiwon-lee left a comment

Choose a reason for hiding this comment

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

저도 여러명이랑 채팅하는 기능을 구현하는데 시간을 많이 쏟았던 것 같아요ㅠㅠ
그래도 이번 과제도 잘 해내셨군요 수고하셨어요 ㅎㅎ
근데 제 노트북에서는 스크롤을 해야 전체 화면을 확인할 수가 있어서 그 부분만 조금 수정해주시면 전체적으로 보기 더 좋을것 같습니다!

Comment on lines +6 to +9
interface LastMessage {
text: string;
timestamp: Date;
}

Choose a reason for hiding this comment

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

이렇게 마지막 메시지를 가져오기 위해 따로 인터페이스를 만드는 것보다는, 현재 가지고 있는 인터페이스를 최대한 활용하는 방법이 더 좋을 것 같아요!

const lastMessage = messages[messages.length - 1];

이렇게 메시지 배열이 있다면, 마지막 요소르 가져오는 식으로 하면 될 것 같습니다.

Comment on lines +15 to +17
const handleBackClick = () => {
navigate('/chatlist');
}

Choose a reason for hiding this comment

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

이렇게 따로 네비게이션 관련 함수를 만들어서 주소 이동을 하도록 해주셨네요!
이 방법말고도 를 활용하는 방법도 있더라구요.
이 방법을 쓰면 굳이 주소 이동을 위한 함수를 작성하지 않아도 되어서 저는 덜 번거로웠던 것 같아요

useNavigate도 많이 활용한다고 하는데 찾아보니 로그인하지 않은 사용자가 프로필 페이지에 접근하려고 하면, 로그인 페이지로 리디렉션 하는 경우에 주로 쓴다고 하더라구요. 저도 이번 키question공부하면서 알게되었습니다 ㅎㅎ

Comment on lines +32 to +34
const getIconStyle = (path: string) => {
return activePath === path ? "#AB78FF" : "#666666";
}

Choose a reason for hiding this comment

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

글씨 색을 경로와 일치하는지에 따라 다르게 두셨네요!
약간 하드코딩느낌이 있어서 boolean값을 활용한다면 좀 더 깔끔하게 구현할 수 있을 것 같습니다.

Comment on lines +21 to +24
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? '오후' : '오전';
const formattedHours = hours % 12 === 0 ? 12 : hours % 12;

Choose a reason for hiding this comment

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

이거 아래처럼 쓰면 '오후','오전' 알아서 맞춰주더라구요!

Suggested change
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? '오후' : '오전';
const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
const formattedTime = new Date().toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
}),

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