Skip to content

[4주차] 황영준 과제 제출합니다.#12

Open
YJ0623 wants to merge 39 commits intoCEOS-Developers:masterfrom
YJ0623:YJ0623
Open

[4주차] 황영준 과제 제출합니다.#12
YJ0623 wants to merge 39 commits intoCEOS-Developers:masterfrom
YJ0623:YJ0623

Conversation

@YJ0623
Copy link
Copy Markdown

@YJ0623 YJ0623 commented Apr 9, 2026

  1. 프로젝트 배포 링크: https://react-messenger-23rd-l2f71g3de-yj0623s-projects.vercel.app/
  2. QA 진행 노션 링크: https://www.notion.so/2-32d8b031c24d804ea74df3ef09995c0b

1. OOP 패러다임을 빌려온 설계, 그러나 결국 FP로 귀결되는 아키텍처

OOP(Object Oriented Programming) 패러다임은 객체가 자신의 상태를 캡슐화하고, 외부와는 메서드를 통해 상호작용하는 프로그래밍 방식입니다. 저는 항상 그랬듯 컴포넌트와 상태 관리 구조를 설계할 때 이 OOP 패러다임을 일부 차용하였습니다.

첫 번째 설계의 출발점은 '데이터 모델의 명확한 정의'였습니다. User와 Message라는 도메인의 타입을 엄격하게 정의하여 데이터의 형태를 객체처럼 통제했습니다.

그리고 이 데이터들을 관리하기 위해 Zustand를 도입하여 useChatStore라는 전역 스토어를 구축했습니다. 이 스토어는 마치 단일 인스턴스(객체)처럼 존재하며, UI 컴포넌트들은 스토어의 내부 상태를 직접 수정하는 대신 sendMessage나 readMessage 같은 액션 메서드를 호출하여 상태 변경을 요청합니다. 이는 객체지향의 캡슐화와 메시지 패싱 원칙을 따랐습니다.

하지만 이 설계는 결국 함수형 프로그래밍(Functional Programming)으로 귀결됩니다. 스토어 내부의 액션 메서드들은 객체지향처럼 원본 데이터를 직접 수정(Mutation)하지 않습니다. 대신, 철저하게 불변성을 지키며 기존 상태를 복사해 완전히 새로운 상태 객체를 반환하는 순수 함수 형태로 작성되었습니다.

예를 한 번 들어보자면, 다음 두 코드를 보면 되겠습니다.

class ChatStore {
  sendMessage(text) {
    const room = this.chatRooms.find(r => r.id === this.currentRoomId);
    // 원본 배열에 직접 push
    room.messages.push(newMessage); 
  }
}

다음과 같은 코드는 원본 배열을 직접 변경함으로써 객체 내의 상태를 메서드로 변경하는 방식이기 때문에 OOP원칙을 철저하게 따랐다고 볼 수 있겠지만, React 자체가 기존의 클래스 구조의 설계 패러다임에서 벗어나고 FP를 지향하는 라이브러리였기 때문에 함수형 프로그래밍을 다음과 같이 Zustand로 정의하였습니다.

sendMessage: (text: string) => set((state) => {
  const updatedRooms = state.chatRooms.map((room) =>
    room.id === state.currentRoomId
      // 원본을 건드리지 않고, 기존 배열을 복사(...)해서 새로운 배열을 만듦
      ? { ...room, messages: [...room.messages, newMessage] } 
      : room
  );
  return { chatRooms: updatedRooms }; // 완전히 새로운 객체를 반환
})

결과적으로 겉으로 보이는 인터페이스는 컴포넌트 간의 결합도를 낮추는 OOP의 캡슐화를 띠고 있지만, 실제 내부에서 데이터를 다루고 렌더링을 트리거하는 방식은 FP의 불변성을 지키고 있는 아키텍처라고 설명할 수 있습니다.

2주차 과제로 넘어가고 '채팅방'이라는 새로운 타입을 정의하게 되면서, 1주차 과제에서 '확장성을 생각하며 구현하라'는 조건을 만족하기 위해 기존에 만들어둔 User와 Message 타입을 폐기하거나 억지로 수정하지 않았습니다. 대신, 객체지향에서 클래스가 다른 객체의 인스턴스들을 배열로 관리하듯이, 기존 타입들을 조립하여 ChatRoom이라는 새로운 타입을 만들었습니다.

export interface User {
  id: string;
  name: string;
  profileKey: string; // 2차 과제에서 1대1 채팅이 아니라 유저가 여러명으로 늘어나며 추가함
}

export interface Message {
  // 현재 내가 보고 있는 id와 senderId가 일치하면 내 시점으로, 그렇지 않으면 상대가 보낸 메시지로 보이게, 근데 구현안함..
  id: string;
  senderId: string;
  text: string;
  isRead: boolean;
  timestamp: string;
}

// 2차 과제에서 '채팅방'기능이 생기며 새로 추가한 타입, 
// 이때 완전히 새로운 타입을 정의하지 않고 기존의 타입을 조합하여 확장함(Composition 방식 적용)
export interface ChatRoom {
  id: string;
  isGroup: boolean;
  participants: User[];
  messages: Message[];
  title?: string;
  unreadCount: number;
  lastMessageAt?: string;
}

ChatRoom 타입은 내부에 participants: User[]와 messages: Message[]를 갖는 컴포지션 구조로 설계되었습니다. 상속을 피하고 합성을 사용함으로써, User나 Message의 독립성을 유지할 수 있었습니다.

이를 통해 Zustand 스토어는 오직 ChatRoom 배열만 관리하면 되었고, 채팅방 안에서 유저가 추가되거나 메시지가 쌓이는 로직은 기존 타입의 안정성을 기반으로 유연하게 확장할 수 있었습니다.

2. 디자이너와의 협업

사실 이 부분은 크게 말씀드릴 부분이 없었습니다. 굳이 얘기하자면 Figma에서 화면처럼 만든 컴포넌트를 tailwind css로 import할 수 있는 기능이 있다는 정도..?
https://www.figma.com/community/plugin/1049994768493726219/inspect-export-to-html-react-tailwindcss
이런 플러그인같은거 쓰면 화면에 있는 요소들을 tailwind css로 변환해주는 게 가능은 합니다.
다만, 뷰포트의 크기와 반응성을 전혀 고려하지 않은 요소들을 날것의 코드 그대로 만들어주기 때문에, tailwind CSS가 익숙하지 않으신 분들이 퍼블리싱 윤곽 잡기를 목적으로 사용한다면 어느 정도 도움이 될 수는 있겠습니다. 근데 결국 디자인 토큰이나 레이아웃 내에서의 세세한 상대적 배치를 생각하면 아예 처음부터 짜보는 게 나을 수도 있겠어요.
QA는 1차때 딱 한 번, 2차때도 딱 한 번 진행했고, svg파일이 잘못 import된 상황 외에는 수정할 요소가 따로 없었습니다.
그래서인지 협업이 훨씬 편했어요(제가 디자인하면 좀 구리게 나오는 것 같더라구요).

3. 피드백 반영

3-1. SVG파일을 SVGR파일로 변경하려 해봤는데, stroke의 색만 다르다던가 혹은 fill값만 다르다던가 하는 그런 svg요소가 하나도 없었어서 이번에는 사용하지 않았습니다.

3-2. 상대 경로를 이제는 절대 경로로 import하는 시도를 했고, 굉장히 편하게 잘 사용했습니다. CEOS 프론트엔드 노션 개인프로필 페이지에 관련 링크 및 제 코드를 올려놨으니 여기까지 보신 분들께서는 꼭 확인해주세요

3-3. 폰트 px -> rem 반영했습니다.

3-4. 페이지는 간결하게, 구현은 컴포넌트 내부에서 했습니다. 서로 반드시 주고받아야만 하는 상태만 공유되도록 페이지 내의 코드를 작성했습니다.

export const CallPage = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');

  return (
    // dvh 써줘야 주소창 높이까지 계산해서 화면 꽉 채워짐
    <div className="w-full h-dvh flex flex-col py-1.25">
      <MainChatHeader
        chatTitle="통화"
        onAddClick={() => setIsModalOpen(true)}
      />

      <SearchBar value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />

      <CallProfileList searchQuery={searchQuery}/>

      <NavBar />

      {isModalOpen && <UserSelectModal onClose={() => setIsModalOpen(false)} />}
    </div>
  );
};

3-5. 역할과 책임을 명확히 분리하여, 특정 기능을 수행하지만 컴포넌트 내에 있지 않아도 상관없는 함수들을 모두 util함수로 분류하였습니다.

도움이 많이 되는 과제였습니다.

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.

1 participant