리팩토링 : Promise 패턴을 이용하여 비동기 처리중인 전역 객체에 접근하기
개발 블로그 개발 여정
reactnextjs
Sun Jun 30 2024
이 글은 개발 블로그 개발 여정의 게시글이예요
해당 포스트는 NextJS를 이용하여 개발 블로그를 만들며 작성한 포스트입니다.
기술 블로그의 전체 코드는 🪢 yonglog github 에서 확인 하실 수 있습니다.
- 1 . 개발블로그 제작을 시작하며
- 2 . CI/CD 파이프라인 구성하기
- 3 . tailwind 환경 설정 및 디자인 레퍼런스 찾기
- 4 . / 경로 레이아웃 , 페이지 디자인 생성하기
- 5 . [BUG] Build 시 발생하는 typeError 해결하기
- 6 . MDX를 사용하기 위한 라이브러리들 설치 및 환경 설정
- 7 . Post들이 저장된 FileSystem 파싱하기
- 8 . PageNavigation UI 만들기
- 9 . tag , series Identifier 만들기
- 10 . / 경로 UI 디자인 하기
- 11 . 서버 컴포넌트에 Dynamic Routing 추가하기
- 12 . Post/page.tsx 생성하기
- 13 . 마크다운 파일 코드블록 꾸미기
- 14 . [BUG] Vercel 에 환경 변수 추가하기
- 15 . Loading Suspense 구현하기
- 16 . generateStaticParams 이용해 SSR 에서 SSG로 넘어가자
- 17 . SSG를 이용한 블로그에서 테마 변경하기
- 18 . 인터렉티브한 사이드바 만들기
- 19 . 기술블로그에 giscus를 이용하여 댓글 기능을 추가하기
- 20 . 기술 블로그의 SEO를 최적화 하기 위한 방법 Part1
- 21 . 기술 블로그의 SEO를 최적화 하기 위한 방법 Part2
- 22 . 바닐라 자바스크립트로 깃허브 OAuth 를 구현해보자
- 23 . 라이브러리 없이 깃허브 API를 이용해 댓글창을 구현해보자
- 24 . 리팩토링 : Promise 패턴을 이용하여 비동기 처리중인 전역 객체에 접근하기
- 25 . NextJS로 만든 기술블로그에서 검색 기능 구현하기
현재 상황은 어떨까 ?
너무나도 무거워져버린 @/lib/post.tsx
mdx
파일들을 처리하는 다양한 메소드가 담긴 @/lib/post.tsx
에 존재하는 파일은 위 이미지와 같이
다양한 컴포넌트들에서 호출되어 사용된다.
이 때 다양한 장소에서 호출 되는 메소드들이 서로 의존성이 존재 하지 않는다면 괜찮겠지만 현재의 @/lib/post.tsx
내부를 살펴보면
내부 메소드들이 parsePosts 와 얼기설기 의존성을 가지고 있다.
위의 이미지처럼 MDX
파일들을 담은 배열을 반환하는 parsePosts
메소드에 직접적이건, 간접적이건 의존성을 가지고 있는 모습을 볼 수 있다.
그 말은 해당 메소드들이 여러 컴포넌트에서 무작위적으로 호출된다면 parsePosts
들도 그 수에 맞춰 매번 호출된다는 것이다.
이로 인해 parsePosts
내부에서 존재하는 비동기적으로 Github API
요청을 하는 부분에서 race condition
문제도 발생했고 이를 막기 위해 flag
를 동기적으로 설정하여 문제를 해결했었다.
해당 트러블 슈팅 경험은 🪢 API요청과 async/await를 잘못 생각하면 겪을 수 있는 트러블 슈팅 경험 에서 볼 수 있다.
게시글에서도 모범 사례가 아님을 이야기했다.
근본적으로 생각해보면 여러 컴포넌트에서 호출하는 다양한 메소드들이 의존성을 가져야 하는 것은 parsePosts
메소드의 행위가 아닌 parsePosts
가 반환하는 MDX파일들을 담은 반환값이다.
그렇다면 parsePosts
가 반환하는 값을 전역 객체로 생성하고 다른 메소드들에서 해당 메소드를 인수로 받거나 참조하면 되는거 아닌가 ? 라고 단순히 생각 할 수 있다.
하지만 parsePosts
가 반환하는 값은 Promise
이기 때문에 반환 값이 resolve
되기 이전까지 다른 메소드들을 호출하지 못하도록 신경 써야 한다.
그래서 그런점들을 신경 써서 parsePosts
가 반환하는 값을 다른 메소드들이 자유롭게 참조 할 수 있도록 리팩토링 해보도록 하자
사용하고자 하는 디자인 패턴 : 프로미스 패턴
프로미스 패턴은 비동기적으로 resolve 되는 객체 A가 존재 할 때
해당 객체 A를 참조하는 메소드 등이 A 객체가 resolve 된 이후 실행 되는 것을 보장하는 형태이다.
이 때 특징적으론 Promise 객체를 생성 할 때 resolve 시키는 callback 함수를 Promise 생성 이후의 순간으로 빼줘버리는 것이다.
이는 프로미스 객체의 특성을 이용한 것으로 예시를 들어보자면 다음과 같다.
해당 패턴은 정말 내가 원하는 그대로를 모두 지원한다. 하나씩 리팩토링 해보자
@/lib/post 파일을 어떻게 쪼갤까 ?
세가지 모델로 역할을 분리해보자
@/lib/post
파일에는 여러 역할을 하는 메소드 들이 서로 섞여 존재한다.
실제 컴포넌트에서 호출 되는 메소드들도 존재하고 , 다른 메소드 내부에서 사용되는 유틸적인 기능을 가진 유틸 메소드들도 존재한다.
이 때 이런 역할을 갖는 메소드들을 하나의 페이지에서 관리 할 것이 아니라 역할에 맞춰
- 다른 메소드를 구성하는데 도움이 되는 유틸 로직 : PostUtilsModel
- 실제로 mdx 파일들을 가져오는 것과 관련된 로직 : PostParserModel
- 컴포넌트에서 호출되어 mdx 파일을 전달하는 로직 : PostProviderModel
이렇게 세 가지 모델들로 나눠서 설계하였다.
이 때 PostUtilsModel
클래스는 생성자를 생성하지 않고 단순히 PostParserModel , PostProviderModel
에서 사용 되는 유틸 함수들을 정의해둔 클래스이며 상속을 통해 필수 로직이 아닌 것들을 캡슐화 하여 전달해주는 역할을 한다.
또 , PostParserModel
과 PostProviderModel
은 build
시 딱 한번만 생성되며 PostProviderModel
은 생성된 PostParserModel
을 인수로 받아 mdx
파일들을 컴포넌트들에게 전달한다.
자세한 것들은 코드를 통해 설명하도록 하겠다.
필요한 유틸 함수들을 캡슐화 해둔 PostUtilsModel
다음과 같이 여러 모델에서 공통적으로 사용되는 유틸 함수들을 담은 PostUtilsModel
클래스가 존재한다.
메인 로직이 아닌 유틸 로직들을 해당 메소드에 정의 해줌으로서 다른 모델들에선 필수 로직만 존재 할 수 있도록 하였다.
Promise 객체를 생성해두는 PostParserModel
이 부분이 이번 리팩토링에서 가장 핵심이 되는 부분인데 PostParserModel
에서
비동기적으로 준비가 되는 mdx
파일들을 담는 this.Posts
부분을 단순한 Promise
객체로만 생성해두고 resolve
시킬 콜백 함수를 this.resolvePosts
가 참조하도록 하여 외부로 빼주었다.
이후 해당 resolvePosts
함수를 비동기적으로 모든 mdx
파일들을 가져온 이후 resolve
시켜주는 모습을 볼 수 있다. (61번째 줄)
이후 postParser
모델을 생성자로 생성 한 후 export
시켜주었따.
postParser
는 생성 되는 시점에서 contructor
내부에서 호출된 prasePosts
로 인해 this.Posts
를 pending
상태로 만들어 둘 것이다.
실제 컴포넌트들에게 값을 전달하는 PostProvider
이후 현재 mdx
파일들을 불러오고 있는 인스턴스인 postParser
를 인수로 받는 postPovider
를 생성해주었다.
해당 클래스는 postParser
의 Promise
객체인 Posts
를 실제 컴포넌트들에게 전달하는 역할을 한다.
이 때 각 메소드들에서 await this.posts(postParser.Post 와 같다.)
를 항상 해줌으로서 fulfiled
된 상태인 Promise
객체를 사용하는 것을 항상 보장해줄 수 있다.
실제 사용 예시
무엇을 얻었을까 ?
우선 매우 무겁던 하나의 파일을 여러 파일들로 나눠줌으로 인해서 가독성이 올라간 것에 대해서 큰 수확인 것 같다.
사실 처음에는 parsePosts
메소드가 반복적으로 호출되지 않고 빌드타임시 딱 한번만 호출되기 때문에 배포 시간도 짧아질려나 ? 했는데
parsePosts
메소드 자체가 많이 무거운 함수가 아니기 때문에 (비동기 요청만 없다면) 크게 시간이 줄지는 않았다. 네트워크 상황도 많이 영향을 미치는 것 같기도 하고 말이다.
그래도 postParser
인스턴스 내부에 Post
값들을 캐싱해둘 수 있기 때문에 해당 캐싱된 값을 이용해 다양한 기능들을 추가 해줄 수 있을 것 같다.
예를 들어 검색 기능이나, GPT 를 이용한 요약 및 번역 기능 같은 것들을 말이다.
만약 이처럼 리팩토링을 해두지 않았다면 포스트에 접근하고자 하는 API 요청이 올 때 마다 매번 parsePosts
메소드가 완료 될 때 까지 기다려야 했을텐데 이제는 그러지 않아도 된다는 점에서 큰 수확인 것 같다. :)
다만 나는 디자인 패턴을 어깨 너머로만 보았을 뿐 열심히 공부해보지 않았기 때문에 해당 패턴이 올바른 패턴인지 확실치 않다. 나중에 시간내서 디자인 패턴을 따로 공부해보고 염두해서 개발해봐야겠다. :)