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함수로 분류하였습니다.

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

Review Q: Next.JS 13 이후의 App Router 방식과 기존 Page Router의 차이점, 디렉터리 구조 변화에 대해 설명해 주세요.

-Next.js 공식 문서 발췌-
버전 13에서 Next.js는 공유 레이아웃, 중첩 라우팅, 로딩 상태, 오류 처리 등을 지원하는 React Server Components 기반의 새로운 App Router를 도입했습니다.

App router 이전에 React Server Components가 무엇인지 알아야 합니다.

-Next.js 공식 문서 발췌-
React Server Components를 사용하면 UI를 서버에서 렌더링하고 선택적으로 캐시할 수 있습니다. Next.js에서는 렌더링 작업이 경로 세그먼트별로 분할되어 스트리밍 및 부분 렌더링이 가능합니다. 리액트 서버 컴포넌트는 다음과 같은 방법으로 구현합니다.

  1. 정적 렌더링
  2. 동적 렌더링
  3. 스트리밍

정적 렌더링은 빌드 타임 또는 데이터 재검증 이후 백그라운드에서 경로가 렌더링됩니다. 따라서 사용자에게 개인화된 컴포넌트가 없는 공통 페이지 등에서 활용 가능합니다. 물론 페이지 내에 사용자 상호작용이 필요한 클라이언트 컴포넌트가 포함될 수 있지만, 이 경우에도 서버에서 초기 HTML을 미리 정적으로 렌더링해 두기 때문에 빠른 초기 로딩과 SEO 이점을 유지할 수 있습니다.

그리고 동적 렌더링에서는 경로가 요청 시 각 사용자에 대해 렌더링됩니다.
따라서 동적 렌더링은 매 요청마다 서버 리소스를 소모하지만, 사용자별로 개인화된 데이터나 실시간으로 변하는 최신 정보를 제공해야 하는 경로에서 필수적으로 사용됩니다.

마지막으로 스트리밍은 페이지를 컴포넌트화해서 한 페이지 내에 여러 컴포넌트, 즉 페이지를 조합함으로써 LCP(사용자에게 첫 화면이 보이기까지의 시간)를 향상시키는 방식이라고 이해하면 되겠습니다. 그래서 렌더링 작업을 청크로 나누고, 각 청크가 렌더링에 성공할 때마다 성공한 것들 먼저 페이지에 렌더링함으로써 LCP를 비약적으로 향상시킬 수 있습니다.

그렇다면 다시 앱 라우팅 방식의 설명으로 돌아와서, 폴더 구조와 경로 세그먼트와 디렉터리 간 작용은 다음과 같습니다.
image
디렉터리의 구조는 트리 구조로 형성되어 링크를 구성하고, 각 노드가 경로 세그먼트로 매핑되는 구조입니다.

또한 특수 파일 경로를 지정함으로써 페이지 내에서 특수한 상호작용(에러, 로딩 등)이 일어날 때 렌더링할 페이지도 선택이 가능합니다.
image

게다가 동일한 레이아웃 내에서 한 페이지 이상의 페이지들을 한 곳에 묶어 동시에 또는 조건부로 렌더링할 수 있습니다. 이것은 아까 설명한 스트리밍과도 밀접하게 관련됩니다.
image

이렇듯 Next.js는 기존의 CSR(Client-Side Rendering) 중심의 리액트 SPA 방식과는 달리, 각 컴포넌트의 렌더링 환경(서버/클라이언트)과 캐싱을 자유자재로 설정할 수 있고, 디렉터리의 경로를 수평 구조가 아닌 Nested구조로 설정함으로써 디렉터리의 각 노드를 페이지의 세그먼트로 설정합니다. 그리고 각 폴더 내에 특수 파일 혹은 여러 서브페이지들을 구성하여 한 페이지 내에 묶어내는 렌더링 및 스트리밍을 사용함으로써 사용자에게 보다 빠른 경험을 제공하는 데에 큰 의의를 두고 있습니다.

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