hi-hoi

값으로 다뤄본 Promise 회고: 함수형 리팩토링의 가능성

📌 읽기 전에

📚 JavaScript 문법이나 Promise의 기본적인 작동 원리에 대한 설명은 생략합니다. 이 개념이 익숙하지 않다면, 먼저 MDN Promise 문서를 참고하는 것을 권장드립니다.


🖐️ 서론

 최근 『멀티 패러다임 프로그래밍』이라는 책에서 비동기에 대한 질문을 읽었습니다.

“실제 업무에서 new Promise()를 직접 사용해 본 경험이 있나요?”

  예전에 회사에서 채팅 모듈 작업 시, 시간 압박으로 충분한 설계를 하지 못했던 기억이 납니다. 이후 다양한 비동기 처리 방식을 접하며 그 선택에 아쉬움이 생겼습니다. 그때의 선택을 돌아보고, 고민해 본 개선점을 공유하려고 합니다.


✅ 채팅 모듈

모듈 개발에 앞서서 다음과 같은 구상을 했습니다.

  • 최소 4개의 서로 다른 서비스에서 사용될 예정입니다.

  • 외부 SDK를 캡슐화한 react custom hook을 제공해 hook의 인터페이스만 알면 사용할 수 있도록 설계합니다.

  • 채팅방, 채팅 메시지 등을 제어할 비동기 메서드가 제공되어야 하고 확장성을 고려해 설계합니다.

  • 샘플 코드는 아래와 같습니다.

const getMessages = (limit = 20): ChatMessagePromise<ChatMessage[]> => {
  const promise = new Promise<ChatMessage[]>(async (resolve, reject) => {
      const channel: GroupChannel = await chat?.chatInstance?.groupChannel.getChannel(
        chatRoom.id,
      )!;

      const messageCollectionParams: MessageCollectionParams = {
        startingPoint: Date.now(),
        prevResultLimit: limit,
      };
      const createdMessageCollection = channel.createMessageCollection(messageCollectionParams);

      const fetchedMessageList = await createdMessageCollection.loadPrevious?.();
      await channel.markAsRead();

      const messages = fetchedMessageList.map((message) =>
        transformMessage(
          message,
          channel,
        ),
      );

      resolve(messages.reverse());
  });

  return new ChatMessagePromise(promise);
};
  • getMessages, getMessagesMore, sendMessage 등의 함수가 제공되고, 각 함수는 모두 ChatMessagePromise를 반환합니다.
  • ChatMessagePromise, ChatRoomPromise 등을 나눠서 설계하였고 채팅과 관련된 공통 Promise로 BasePromise를 정의합니다.
  • BasePromise는 onSuccess, onError 등의 후속 메서드를 가지게 하였습니다.

🧠 설계 의도: 왜 Custom Promise 체이닝 구조를 선택했을까?

  1. 모든 작업이 순차적 진행(예: 채팅방 정보를 획득한 후 메시지를 요청)이어야 한다고 예상했습니다.
  2. ChatMessagePromise와 같이 도메인별 인터페이스를 구성하면 향후 확장이 용이합니다.
  • 그런데 생각해 보면 이 설계는 무언가 잘못된 것 같습니다.  아래 코드는 실제 호출되는 형태입니다.
sendMessage({
  type: 'notice',
  noticeType: 'AD_ACCEPTANCE',
  content: {
    influencer: influencerProfile,
    schedule,
    additionalInfo,
  },
})
.onSuccess(() => {
  notifyNewMessage({
    campaignId,
    influencerId: influencerProfile.id,
  });
  onSendMessage?.();
});
  • 위 코드의 onSuccess는 Promise.then과 다르지 않습니다. 즉 활용 방법이 onSuccess정도라면 기본 Promise에 비해 유용성이 없습니다.
  • 오히려 같은 흐름이라면 async - await로 처리하는게 대부분의 개발자에게 더 익숙하고 가독성도 좋습니다.

// async - await
const sentMessage = await sendMessage();
notifyNewMessage({});
onSendMessage?.();

📖 회고

설계 철학은 옳았던 것 같다.

  1. 비동기를 값으로 다룬다는 개념은 충분히 유효합니다.
  2. 역할 분리는 객체지향 원칙에 부합합니다.
  3. 기능이 늘어나는 등 기획 변경에 대한 확장이 용이합니다.

그러나 아쉽다면

  1. 많은 메서드가 제공되지 않으면서 체이닝의 이점을 살리지 못했습니다.
  2. Promise를 반환한 뒤 다시 Promise를 구성하는 등 핸들링 과정의 코드가 지저분합니다.

🔁 리팩토링: 함수형 스타일을 결합해서

class ChatMessagePromise<T> extends BasePromise<T> {
  constructor(props: Promise<T>) {
    super(props);
  }
  // ...
}

class FxChatIterable implements AsyncIterable {
//... map, filter, reduce 등을 지연 평가 적용해서 처리 (AsyncIterable)
}


const getMessages = async (limit = 20): ChatMessagePromise<ChatMessage[]> => {
  const channel: GroupChannel = await chat?.chatInstance?.groupChannel.getChannel(
          chatRoom.id,
  )!;

  const messageCollectionParams: MessageCollectionParams = {
    startingPoint: Date.now(),
    prevResultLimit: limit,
  };

  await channel.markAsRead();

  const createdMessageCollection = channel.createMessageCollection(messageCollectionParams);

  return new FxChatIterable(createdMessageCollection.loadPrevious?.())
          .map((message) => transformMessage(message, channel))
          .reverse();
};

참고: 지연 평가란: 모든 값을 즉시 평가하는게 아닌, 필요한 시점에 평가할 수 있도록 하는 방식입니다.

  • 변경하게 되면 이점은 다음과 같습니다.
    1. 지연 평가가 적용되어서 사용처에 따라 원하는 시점에 값을 평가할 수 있습니다.
    2. 반환된 AsyncIterable을 통해 체이닝을 더욱 효율적으로 구성할 수 있습니다.
    3. 전체 메서드들의 구현 로직이 좀 더 선언적이 되어 가독성이 향상되고 일관성을 지킬 수 있습니다.

 이 구조는 JavaScript의 'AsyncIterable' 패턴과 'FxTs', 'RxJS'와 같은 스트림 API에서 영감을 받은 형태입니다. 비동기를 처리가 가능한 map, filter, reduce 등의 고차 함수를 체이닝할 수 있어, 선언적 제어가 가능합니다.

// ex)
await FxChatIterable.from(messages)
  .filter((m) => !m.isDeleted)
  .map(transformMessage)
  .toArray();
  • 🚧 다만 실제 적용에는 이르지 못했습니다.

 설계 방향이 아무리 옳더라도, 협업 중인 코드 베이스에 곧바로 적용할 수는 없습니다. 협업에서는 팀 컨벤션에 대한 이해와 합의가 무엇보다 중요합니다. 새로운 접근 방식이 낯설지 않도록 먼저 함수형 패러다임을 팀에 잘 공유하는 작업부터 진행하려고 합니다.


🧭 끝으로

 저는 처음 개발을 배울 때부터 함수형 프로그래밍을 좋아했습니다. 고차 함수와 커링, 파이프 기법을 보면서 깔끔하게 작성되는 코드를 접하고, 그런 코드를 직접 만들어보면서 많은 재미를 느꼈습니다.

 내부 로직을 구성할 때는 함수형이 유용하지만 팀 단위 협업에 쉽게 도입하기는 어려울 것 같았습니다. 하지만 그건 제 선입견에 불과했고 실제로는 적절히 잘 활용한다면 코드의 표현력이 훨씬 좋아지는 것 같습니다.

 『멀티 패러다임 프로그래밍』은 함수형 프로그래밍에 관심을 갖게 해 주신 개발자님이 출간하신 신간입니다. 책은 예전에 자바스크립트로 소개해주셨던 예제들을 타입스크립트, 하스켈, C# 등 다양한 언어로 현대적인 패러다임에 맞게 잘 안내되어 있습니다. 많은 인사이트를 얻은 덕분에 희미해졌던 열정이 되살아났습니다.

 클래스 구조에도 잘 어울리고 비동기 처리에도 효과적으로 활용 가능한 함수형 프로그래밍을 보면서 새삼 기본기의 중요성이 생각납니다.

 개발자로서의 성장은 결국 기본기에서 시작되는것 같습니다. 앞으로도 "더 나은 코드"를 고민하며 성장하고 싶습니다.

 이 글이 같은 고민을 하고 있는 분들께 작은 영감이 되었기를 바랍니다.

댓글 목록

댓글을 불러오는 중...

댓글 남기기

Loading...