본문 바로가기
Projects,Activity/DizzyCode(React)

[DizzyCode] 디스코드 클론코딩, 시작!

by 그냥하는거지뭐~ 2024. 7. 2.
목차
- 프로젝트의 시작
- 우리끼리의 용어 
- 아키텍처 선정: FDA
- 협업을 위한 프로젝트 기본 세팅
- 상태관리: React Query + Zustand

 

1. 프로젝트의 시작

5월 말 황00씨로부터 간단하게 프로젝트 하나 할 건데 할래?라고 제안이 들어왔다. 7월은 절대 안 넘어갈 거고 6월까지 무조건 끝낼 수 있다고 했다. 심지어 6월에는 기말고사가 있는데 시험준비와 병행을 해도 충분히 끝낼 수 있을 정도로 간.단.한. 프로젝트라고 했다. 이 말을 믿어버렸다. (취업사기.. 오히려좋아?
프로젝트는 디스코드를 클론코딩하는 건데, 안 그래도 채팅을 만들어보고 싶었어서 흥미롭게 시작할 수 있었다. 그리고 클론코딩이라 기획 단계를 스킵할 수 있어서 기술에만 집중할 수 있다는 점이 특히 마음에 들었다. 백은 2명, 프론트는 1명이라 다급하게 프론트도 한 명 더 영입했다. 이 프로젝트로 얻어가고 싶은 점은 새로운 기술들을 마음껏 써보되 충분한 이해를 기반으로 품질 높은 코드를 짜는 것에 집중하는 것이다. 
 
디스코드에는 뜯어볼수록 세심한 기능들이 정말 많다. 채팅에서 thread를 만들어서 따로 채팅방을 분리할 수 있는 기능이라던지, 같은 방에 있어도 채팅방에 접근할 수 있는 권한이 다르다던지.. 이 모든 것을 완전히 똑같이 만드는 것은 무리가 있기 때문에 어디까지 구현할 것인가를 명확하게 정하고 시작할 필요가 있었다. 그래서 간단하게나마 피그잼으로 flow chart를 만들었다. 
 

확실히 정리가 되는 느낌!

 
 


2. 우리끼리의 용어

디스코드 화면

Room | Dizzy Code
디스코드를 처음 시작할 때 서버를 생성하거나, Explore 페이지에서 원하는 서버에 입장할 수 있다. 우리는 이 서버를 '방(Room)'이라고 칭했다. 
 
Category | 정보, 채팅 채널, 음성 채널
성격이 비슷한 채널들끼리 그루핑할 수 있다. 이를 '카테고리'라고 했다. 카테고리는 Toggle할 수 있다. 
 
Channel | 일반, 세션-계획, etc
한마디로 채팅방이다. Channel을 생성할 때는 일반 채팅방과 화상통화를 할 수 있는 방 중 하나를 선택해서 생성한다. 

export type ChannelType = 'CHAT' | 'VOICE';

 

방 안에 카테고리 안에 채널!

 
DM
디스코드는 방에서 방 멤버들과 1:n으로 채팅할 수도 있지만, 별개로 DM 페이지에서 친구와 1:1 채팅도 가능하다. DM을 하나의 Room으로 보고, DM에는 카테고리 없이 채널들만 존재한다. 
 


3. 아키텍처 선정: FDA

플젝을 시작할 때마다 고민하는 아키텍처 선정.. 후보는 FDA, atomic design pattern, FSD였는데, FDA(Feature Driven Architecture)를 선택했다. 그 이유는 다음과 같다. 
 
1. 스타일링 
스타일링은 Chakra UI를 적극적으로 활용하기로 했다. Chakra UI에서는 컴포넌트를 커스터마이징할 수도 있어서 공통 컴포넌트를 만들어 관리할 필요가 거의 없어졌다. 컴포넌트를 재사용할 일이 확연히 줄어들어 atomic design pattern은 과할 수도 있겠다는 판단이 섰다. 
 
2. 페이지보다는 기능
디스코드를 보면 페이지가 많지 않다. 대신 한 페이지에 담겨 있는 기능들이 정말 많다. 그래서 페이지로 구분하기보다는 기능으로 구분해야겠다고 생각했다. hooks, utils, api 등등도 기능별로 공유하지 않고 서로 다른 기능들에 영향을 미치지 않는다. 
 
3. 프로젝트 규모가 크지 않다 (이땐 몰랐지.. )
프로젝트 규모가 크면 FSD도 적용해 볼 만 한데, FDA로도 충분하다고 판단했다. 
 

// 대충 요런식
...
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── pages/
│   │   │   ├── LoginPage.tsx
│   │   │   └── RegisterPage.tsx
│   │   └── types.ts
│   │
│   ├── chat/
│   │   ├── components/
│   │   │   ├── ChatList.tsx
│   │   │   ├── ChatInput.tsx
│   │   │   └── ChatMessage.tsx
│   │   ├── hooks/
│   │   │   └── useChat.ts
│   │   ├── pages/
│   │   │   └── ChatPage.tsx
│   │   └── types.ts
│   │
│
├── components/ // 공통 컴포넌트 
│   ├── Button.tsx
│   ├── Input.tsx
│
...

4. 협업을 위한 프로젝트 기본 세팅

#ESLint #prettier #husky #lint-staged
 
여러 팀원들과 프로젝트를 진행할 때 코드의 일관성을 유지하는 것은 굉장히 중요하다. 이를 위해 ESLint, Prettier, Husky, Lint-staged와 같은 도구들을 사용한다. 
 
ESLint는 코드의 오류를 잡아내고 코드 스타일을 일관되게 유지하도록 도와준다. 깜짝 놀랐던 점은, useEffect의 dependency array에 무엇이 빠졌고, 무엇이 불필요하게 들어갔는지까지 알려준다. .eslintrc.json 파일에서 parser, plugins, rules 등등을 정할 수 있다. Prettier로는 탭의 줄 간격이나 줄바꿈과 같은 스타일들을 정할 수 있다. 
 
Husky를 사용하면 git hooks를 쉽게 관리할 수 있다. 커밋하기 전이나 푸시하기 전에 원하는 작업을 할 수 있다. 우리 프로젝트에서는 커밋 전에 ESLint, Prettier 검사를 해서 통과하지 못하면 커밋이 되지 않게끔 설정했다. Lint-staged는 git stage에 추가된 파일에만 린트 작업을 수행하도록 하는 도구이다. 변경된 코드만 린트를 수행하는 것이다. 
 
통과하지 못하면 이런식으로 에러를 띄워주고 커밋이 기각된다.

 
성공하면 요로케!

 


5. 상태 관리: React Query + Zustand

이전 프로젝트에서 Redux로 상태관리를 해본 결과 store가 너무 크고 복잡했고, store 안에서는 API 호출과 관련된 코드의 비중이 크다는 문제점이 있었다. react query와 zustand의 조합으로 server state와 client state를 분리할 수 있다. API fetching과 같은 서버 상태 관리는 react query에서 관리하고, 모달 open, close와 같이 client에서만 관리하는 state는 zustand가 담당하는 것이다. 
 
예시로, 우리 프로젝트에서 방과 관련된 데이터들을 다음과 같이 관리하고 있다. 
 
react query로 내가 속한 방에 대한 정보를 fetch 해서 캐싱처리한다. 

// get rooms
const { data: rooms } = useQuery<IRoom[], Error>({
    queryKey: ['rooms'],
    queryFn: getRooms,
});

 
이후에 데이터를 읽어올 때는 추가적으로 fetch하는 게 아니라 캐시에서 가져온다.  

const queryClient = useQueryClient();
const myRooms = queryClient.getQueryData<IRoom[]>(['rooms']);

 
 
Zustand에서는 현재 채널에 대한 정보만 담고 있다. 

const useRoomStore = create<IRoomState>((set) => ({
  currentChannelPath: { roomId: 0, categoryId: 0, channelId: 0 },
  setCurrentChannel: ({ roomId, categoryId, channelId }) =>
    set({ currentChannelPath: { roomId, categoryId, channelId } }),

  currentChannelName: '',
  setCurrentChannelName: (name) => set({ currentChannelName: name }),
}));

export default useRoomStore;

이를 바탕으로 현재 내가 어느 방에 들어와 있는지 UI로 표시하기도 하고, 

const Header = () => {
  const { currentChannelName } = useRoomStore();

  return (
    <Container>
      <ChannelName channelName={currentChannelName} />
    </Container>
  );
};

 
현재 들어와있는 방의 id를 이용해서 react query로 추가적인 데이터 fetching 작업을 하기도 한다. 

 const {
    currentChannelPath: { roomId },
  } = useRoomStore();

const { data: categories } = useQuery<ICatwChannel[]>({
    queryKey: ['catwChannels', roomId],
    queryFn: () => getCategories(roomId),
    enabled: !!roomId,
});

 
 


6. 마치면서 

웹소켓, react query, zustand 등등 사용해보고 싶었던 라이브러리나 다양한 기술들을 마음껏 사용해 보고 있어서 하나씩 알아가는 재미로 살고 있다. 무엇보다 기획을 하거나 어떻게 하면 사용자가 모일까 이런 고민 없이 기술 공부에만 집중할 수 있어서 좋다.
이 글을 쓰는 시점은 채팅까지 구현이 된 상태인데, 할게 산더미다. 무한스크롤 데이터 fetching.. 많은 채팅 데이터들을 프론트에서 어떤 식으로 효율적으로 관리할지.. 비정상적으로 소켓 연결이 끊겼을 때 재연결하는 로직.. 등등.. 그리고 이제 화상회의 구현을 위해 WebRTC를 공부해야 할 차례이다. (1:N 화상...할 수 있겠지..?)