[개발일지] TypeScript, Flux 패턴을 적용한 리팩토링을 하고,,,

김선종·2021년 11월 20일
8

서론

무모하게 14기랑 과제를 함께 진행한다고 선언하면서, 타입스크립트와 Flux 패턴을 처음 접하게 되었다. 나는 할 수 있다라는 작은 자신감으로 시작하고 조금씩 조금씩, 하루에 하나의 기능만을 고쳐나가자고 다짐했었는데, 처음엔 순순히 잘 진행되다가도 나흘을 넘게 한 에러에만 매달려있기도 하는 등, 짧은 일주일동안의 개인 공부였지만 많은것을 느끼게 되었다. 그래도 이제 타입스크립트, Flux 같이 리액트 사용의 입문은 벗어난 것 같아 기분이 좋긴 하네...

TS... 쉽지많은 않았다

타입스크립트... 대세라고는 하는데,,, 뭔 인턴 공고를 보면 프론트엔드 직군은 죄다 타입스크립트 경험을 원한다. 아니 다들 어떻게는 혼자 공부하는거니까 그래도 어떻게는 했겠지?? 라는 마음으로 무작정 덤벼보았는데, 그래도 생각했던 것 보다 엄청나게 고통스러운 과정은 아니었다. 오히려 타입스크립트 생각보다 별거 아니네 라고 거만해 질 수도 있을거 같은 기분. 물론 뭐든 깊게판다면 끝은 없다.

정적 타이핑은 확실히 득이다

그동안은 타입스크립트를 배우려고 생각이 들 때마다

아니 그 수많은 타입들은 대체 어떻게 찾아가지고 일일히 넣으라는 말이지??

라는 생각에 절로 주저하게 만들었었는데,,, 생각해보면 우리는 거의 대부분 C/C++/Java 같이 정적 타입을 갖는 언어를 처음으로 맞이하게 된다. 나또한 그랬고,, 파이썬을 시작으로 동적타이핑 언어에 너무 익숙해져있다 보니 코딩을 할때 변수의 타입이라는거 자체를 고려하지 않게 된지 꽤 된것 같다. 분명 동적 타입은 이점이 크다. 그런데 리액트의 경우엔 확실히 타입이 지정되어 있는게 유리하다는 생각이 든다.

자동완성 너무좋아

특히 컴포넌트에 props를 넘겨줄 때 그 강점이 극대화된다고 생각이 드는데, 개인적으로 props의 이름을 최대한 상세하게 하려고 하기 때문에 이름이 굉장히 길어지는 경향이 있다. 그런 경우에 타입스크립트가 자동완성을 지원해주면서 props를 빠르게 입력할 수 있다는 점은, 정말 타입스크립트에 더욱 빠져들게 만드는 요인이다.


전부 보이지도 않을정도로 긴 props 이름을 (onSearchQueryChange) 자동완성으로 한번에 넣었을 때 쾌감이 장난아니다. JS는 이런거 안된다고...

내가,,, 뭐라고,,, 했더라,,,?

확실히 조금만 어플리케이션 규모가 커져도, object의 구성 요소의 정확한 네이밍이 헷갈리기 시작한다. 나의 경우는 채팅방 목록/채팅방 을 구분해야 했었는데, 아무리 chatrooms___, chatroom___ 으로 구분하자고 나름대로 컨벤션을 정해도 나중에 무지성으로 코딩하다보면 분명 헷갈리기 시작하는 순간이 온다. 그럴 때 또 자동완성의 힘을 빌어, 오브젝트의 정확한 attributes를 불러올 수 있다는 점이 너무좋다.

편-안하다.

아 이게 이런거였구나...

타입스크립트를 통해 기존에 써오던 내장 함수들의 리턴타입을 정확히 알게 되면서 그 동작원리에 대해 제대로 알게 되는 경험까지 했다. 아니 나 이정도면 타입스크립트 홍보대사 아닌가 싶은데,,,
하여튼, 리팩토링 과정을 하면서 Flux 패턴을 적용하기 위해 useReducer를 적용했다. 그래서 useReducerdispatch 함수의 타입을 지정하려고 하는데, 그 부분에서 자꾸 에러가 났었다.


Flux 패턴에서 dispatch함수 자체는 아무것도 리턴하지 않는다. 입력받는 액션에 따라 리듀서에게 동작을 지시할 뿐이다. 나는 이전까지 그냥 dispatcher는 곧 reducer라고 잘못 이해하고 있었다. 그래서 자꾸만 dispatcher의 타입을 React.Reducer<state, action>으로 자꾸 시도하니 에러가 뿜뿜할수밖에,,,

내맘대로 훅- 하고 함수쓰기

애플리케이션에 Flux패턴을 적용하면서, dispatch를 컴포넌트에서 그대로 바로 불러다 사용하기 보다는, 한단계 더 추상화해서 사용하는 것이 더 올바르지 않을까 라는 생각이 들었다. 게다가 꼭 dispatch를 사용하지 않아도 같은 기능을 묶어두는 함수 그룹이 있었기에 더더욱 custom hook의 필요성을 느끼게 되었다.

Flux는 진짜 전설이다

페이스북에서, 페이스북 앱 내의 알림 버그를 수정하기 위해 도입한게 flux패턴으로 알고 있는데, 개인적으로 여러방향으로 난잡하게 데이터의 이동이 있는 기존의 MVC패턴 대비 한 뱡향으로만 동작이 흐르는 flux는 이해만 한다면 훨씬 더 직관적이고 가독성 또한 좋아질 수 있다고 생각한다.

단적인 예로, 내 애플리케이션의 경우에는 기존 구조는 이러했었다.

  • 전체 채팅방 목록을 가지는 state가 최상단 컴포넌트에 존재하고,
  • 각 채팅방 컴포넌트는 해당하는 채팅 부분만 state에서 떼어서 따로 state로 갖는다.
  • 각 채팅 컴포넌트는, 대화 상대방 id와 대화 내용을 가지고 있는데, 여기서 대화 내용만 또 state로 빼서 선언한다.
  • 그래서 채팅을 주고받으면 채팅만 있는 state를 업데이트한다.
  • 채팅방을 나가면 채팅만 있는 state를 개별 채팅방 state에 업데이트하고, 이 state를 또 전체 채팅방 state에 업뎃한다...

정신나갈거같애

그래서 채팅방 state를 context로 선언하고, 리듀서를 통해 업데이트 로직을 매우 간편하게 줄여버렸다.

// chatroomContext.tsx

const chatroomListReducer = (state: chatroomList, action: chatroomListAction): chatroomList => {
  switch (action.type) {
    case 'chatrooms/updateMessage':
      const restChatrooms = state.filter(
        (chatroom) => chatroom.friendId !== action.data.friendId
      );
      return [action.data, ...restChatrooms];
    default:
      return state;
  }
};

이런식으로 리듀서에서 채팅방 컨텍스트에 대한 업데이트 로직을 정의하고,

// useChatroomContext.ts

const updateChatroomList = (chatroom: chatroom) => {
    chatroomListDispatch({ type: 'chatrooms/updateMessage', data: chatroom });
  };

이렇게 custom hook에서 불러온다.

// chatroom.tsx

const handleSendMessage = (event: any) => {
    event.preventDefault();

    // 메세지 내용 없이 전송하려 하면 alert
    if (currentMessage === '') {
      alert('메시지를 입력하세요!');
      return;
    }

    updateChatroomList({
      friendId: parseInt(friendId),
      chats: [
        ...currentChatroom.chats,
        { senderId: currentSendingUser.id, message: currentMessage },
      ],
    }); // 이렇게 간단해졌다.

    setCurrentMessage(''); // input form을 비운다.
  };

실제 컴포넌트에선 이런식으로 사용한다.

context도 결국엔 state다

처음엔 채팅방 목록 컨텍스트와, 개별 채팅방 컨텍스트를 개별로 분리했었다. 그래서 결국 채팅방에 입장하면,

  • 채팅방 목록 컨텍스트에서 현재 채팅방만 필터링해서 채팅방 컨텍스트에 값을 할당한다 (리듀서를 통해)
  • 그 채팅방 목록에서 채팅 목록을 가져온다.
  • 채팅을 치면 채팅방 컨텍스트에 채팅을 업데이트한다
  • 채팅방을 나가면 채팅방 목록 컨텍스트에 해당 채팅방을 업데이트한다.

뭐 결국 컨텍스트만 썼다 뿐이지 기존의 구조랑 거의 같았는데, 문제는 채팅방 컨텍스트에서 채팅 목록을 가져오는 과정이 채팅방 입장과 동시에 이루지지 않았다. 생각해보면 당연한것이, context도 결국 state인데, dispatchsetState처럼 비동기로 동작한다는 것을 왜 자꾸 간과했었는지 모르겠다. 채팅방 컨텍스트에 채팅방을 할당하고 채팅방 컨텍스트에서 채팅 목록을 불러오는 것을 한 함수내에 불러오는 방식으로 하려다 보니, 자꾸 채팅 목록이 안불러와지게 되고 이 문제로만 3일을 내리 고생을 했다.

결국 채팅방 목록 컨텍스트 한개만 유지하기로 구조를 바꿨는데, 이로 인해서

  • 채팅방을 나가면 채팅방 목록 컨텍스트에 해당 채팅방을 업데이트한다.

의 과정이 필요없어지게 되었다. 처음엔, 채팅방을 나갈때 채팅 목록에 채팅을 업데이트하기 위해서 useEffect의 cleanup 함수에 상태를 업데이트 하려고 했는데, 에러가 나는것이었다. 이 또한 생각해보면 당연한게, 어떤 컴포넌트를 언마운트 하면서 그 컴포넌트의 상태를 이용하려고 한다는게 말이 안되는 점이었다.

하여튼 컨텍스트를 한개로 통일하면서 채팅을 주고받는 순간에 전체 채팅방 목록에 업데이트되고, 부수적으로 채팅을 주고받은 채팅방은 채팅방 목록에서 최상단으로 올라가게 되는 효과(?)까지 얻게 되었다. 개꿀

코드 읽기가 편-안 해지는 매직

custom hook을 쓰는 가장 큰 이유는 역시 중복되는 로직을 줄이는것, 그리고 코드의 가독성을 높이는데 있는것이 아닌가 싶다.

위의 사진처럼 채팅방 목록은 친구의 아이디만 가지고 있고, 프로필 사진이나 이름 등의 자세한 정보는 친구 컨텍스트에 저장되어 있다. 따라서 채팅방에서 친구의 프로필 사진을 불러오려면 기존엔 다음과 같은 방식을 사용해야 했었다.

  • 채팅방 컨텍스트의 친구 id를 기준으로 친구 컨텍스트에서 친구를 찾고 거기서 프사를 찾는다.

이 과정이 채팅방과, 채팅방 목록에서 모두 사용되는데 각 컴포넌트에서 각자 코드를 따로 쓰다보니 효율성이 영 꽝이었다. 그래서 custom hook을 통해 중복을 최대한 줄여보았다.

// useUserContext.ts

const getSingleFriend = (friendId: number): friend =>
    friends.find((friend) => friend.id === friendId)!;
// chatroom.tsx

// 채팅방의 parameter로 받은 id는 곧 친구의 id이다.
  const currentFriend = getSingleFriend(parseInt(friendId));

이렇게 한줄로 친구를 불러올 수 있다.

그래서

생각보다 타입스크립트와 flux가 왜 좋고, 이걸 왜 많이 쓰는지를 명확하게 깨달을 수 있는 경험이었다. 만약 아예 새로운 무언가를 만들었다면 이처럼 절실하게 느껴지지는 않았을 것이라고 생각이 든다. 역시 리팩토링을 해보면서 더 배우는것이 맞는거 같다. 다만 시간에 쫓겨 차마 적용하지 못한것들이 있는데,

  • HOC를 통해 컴포넌트 자체의 재사용성 높이기
  • UI 측면의 컴포넌트 재사용성 높이기
  • Form, Searchbar 등의 컴포넌트에서 custom hook 사용하기
  • 친구 추가및 삭제, 채팅방 추가 및 삭제 등 flux 패턴의 용이한 확장성 느껴보기
  • 기존 채팅목록을 localStorage에 저장하기
  • ThemeProvider 사용을 통한 라이트모드/다크모드 전환

못한게 더많은거 같은건 비밀

등을 차차 적용해보면서, 리액트, 타입스크립트에 대해 조금 더 깊게 이해할 수 있기를 바란다.

profile
개발자가 되고싶다 열심히하자

2개의 댓글

comment-user-thumbnail
2021년 11월 21일

타입스크립트와 flux 패턴을 사용했을 때의 장점을 잘 풀어낸 글인 것 같습니다!
많이 배워갑니다 👍👍

답글 달기
comment-user-thumbnail
2021년 11월 21일

많이 배워갑니다 👀👀

답글 달기