Velog
블로그 목록

라이브러리 개발 경험기

0
라이브러리 개발 경험기
npmnext.js

계기

불편하면 자세를 고쳐 앉아

개발 프로젝트를 많이 하다보면 예전에 했던 프로젝트에서 복붙해오는 코드가 꽤 있다.

예를 들면 커스텀 엑시오스 정도?

그런데 이 코드 모듈을 복붙해오는 과정이 어느 순간 부터 상당히 귀찮아 지기 시작했다.

커스텀 엑시오스 뿐만 아니라 toast, modal 등등 내 프로젝트에서 쓰는 공통적인 코드가 점점 많아지기 시작 했기 때문이다.

기존의 라이브러리들

토스트

우선 토스트 메시지의 경우 react-toastify나 antd의 notification, message를 주로 사용했었는데,

이 라이브러리들은 각각의 개성이 너무 뚜렷해서 내 프로젝트의 디자인에 녹이기 어려웠다.

로딩바

next-nprogress-bar 라이브러리가 있었는데 Next.js 15로 버전이 올라가면서 작동이 안되는 문제가 발생했다.

다른 비슷한 라이브러리들도 nprogress 기반이라 똑같이 작동하지 않는 걸로 기억한다.

모달

모달의 경우 antd의 모달을 가장 많이 사용했던 것 같고 @toss/use-overlay도 사용해보았다.

antd는 토스트처럼 개성이 좀 있기도 하고 레이아웃이 헤더, 바디, 버튼(푸터)로 정해져있어서 자유도가 좀 떨어진다.

@toss/use-overlay는 사용하기 좋았는데 뭔가 내가 쓰는 선에서는 좀 더 인터페이스를 단순화하면 좋을 것 같았다.

커스텀 엑시오스

커스텀 엑시오스는 프로젝트별 백엔드 스펙에 따라 다 달라서 라이브러리로 일반화하긴 어려워서 사람들마다 다 만들어서 쓰나보다. 그렇지만 내가 만든건 나만 쓰기 때문에 라이브러리로 만드는데 있어서 어려움은 없었다.

또, Next.js에서 SSR로 데이터를 가져오는 경우와 클라이언트에서 API를 요청하는 경우 인터페이스나 보일러플레이트(SSR에서는 쿠키 가져오기)등 거슬리는 부분이 있어 이 부분을 개선하려 했다.

그래서 너는 뭘 했니

소개하자면

bash
@cher1shrxd/modal
bash
@cher1shrxd/loading
bash
@cher1shrxd/toast
bash
@cher1shrxd/api-client

위의 네 가지 라이브러리를 만들었다.

@cher1shrxd/modal

모달을 쉽고 자유도 높게 만들 수 있는 라이브러리이며, 다음과 같은 방식으로 모달을 열 수 있다.

typescript
modal.open(<Component />)

라이브러리에서는 모달 컨테이너와 오버레이만 제공하고, 다른 기능은 제공하지 않는다.

사용자가 모달 내부 UI만 정의한다면 간편하게 모달을 생성할 수 있다.

typescript
modal.close()
modal.closeAll()

를 통해 모달을 닫을 수 있다.

모달 데이터들을 stack 자료구조 형태로 저장하고 close() 호출 시 하나씩 pop한다. closeAll() 호출 시 stack을 비워 모든 모달을 닫는다.

모달 렌더링의 경우 ReactPortal을 사용했다.

모달을 사용하기 위해서는 layout.tsx 또는 레이아웃 컴포넌트에

typescript
<ModalProvider />

를 추가해야한다.

zustand를 사용하여 모달 정보를 저장하고, 모듈 스코프에 모달 열기/닫기 인터페이스를 정의한 객체를 선언하여 별도의 훅 사용 없이 modal 객체를 임포트하여 사용할 수 있다.

@cher1shrxd/loading

App Router를 사용한 Next.js 프로젝트에서 페이지 이동 시 상단에 로딩바를 표시하여준다.

layout.tsx에

typescript
<LoadingBar />

를 추가한 후,

typescript
const router = useRouter(); // import { useRouter } from "@cher1shrxd/loading"

를 사용하거나

typescript
<Link href="/path"></Link> // import { Link } from "@cher1shrxd/loading"

를 사용하여 페이지 이동 시 로딩 상태를 활성화할 수 있다.

useRouter와 Link 모두 next.js의 기본 훅과 컴포넌트를 래핑하여 구현했다.

zustand에 로딩 상태를 저장하고, Link의 onClick, router의 push, back, replace 메서드에 로딩 상태를 활성화 하는 코드를 추가하여 로딩 중임을 명시한다.

LoadingBar에서는 usePathname과 search params를 이용하여 페이지경로에 변화가 있는지 감지한다.

변화가 감지된다면 로딩 프로그레스를 100%로 변경하고 로딩 상태를 비활성화한다.

사실 라이브러리를 만들게된 계기가 이 모듈 때문이었다. 프로젝트 폴더 구조에 따라 나눠지는 파일이 너무 많아서 복붙하기 너무 귀찮았다.

@cher1shrxd/toast

토스트 메시지는 제목과 내용, 지속시간(옵션)을 인자로 받으며 성공, 실패, 경고, 정보 네 가지의 타입을 제공한다.

typescript
toast.success("성공", "성공한 내용")
typescript
toast.error("실패", "실패한 내용")
typescript
toast.warning("경고", "경고할 내용")
typescript
toast.info("정보", "정보 내용")
Image

toast도 마찬지로 모듈 스코프를 이용해 토스트 이벤트와 데이터를 발생시키고 수신, 수신 해제 등의 인터페이스를 객체의 메서드로 선언하였다.

typescript
<ToastContainer />

layout.tsx에 위 컴포넌트를 추가하면 토스트 이벤트 수신을 시작하고, toast.메서드()를 사용하면 이벤트를 발생시켜 토스트를 보여준다.

@cher1shrxd/api-client

클라이언트, 서버에서의 요청 인터페이스를 통일한 axios 래핑 라이브러리이다.

axios처럼 .get(), .post(), .put(), .patch(), .delete() 메서드로 요청하며, 이 인터페이스는 axios의 인터페이스와 99.8% 동일하게 제작되었다. (0.2%는 아직 찾지 못한 부분)

추가로 SSR시 withCredentials를 활성화해도 쿠키가 담기지 않는 문제가 있어 직접 next/header 패키지의 cookies를 가져와서 사용해야한다. 다만 쿠키를 사용하면 빌드시 DynamicServerError가 발생하기 때문에 필요한 부분에만 쓸 수 있도록 구현을 해야했다. apiClient의 요청 메서드 뒤에 .withCookie()를 체이닝하면 쿠키를 담은 요청을 보낼 수 있도록 구현하였다. next/header를 임포트 하기만 해도 DynamicServerError가 발생하는 것 같아 동적 임포트로 cookies를 가져올 수 있게 하였다.

자세한 사용 방법은 아래와 같다 (npm, github의 README 발췌)

typescript
import { createApiClient } from "@cher1shrxd/api-client";

export const apiClient = createApiClient({
  // 필수
  baseURL: process.env.NEXT_PUBLIC_API_URL!,

  // 선택
  timeout: 10000,
  headers: {
    "X-Custom-Header": "value",
  },
  withCredentials: true,
  debug: process.env.NODE_ENV === "development",

  // 서버사이드 쿠키 설정
  // cookieNames에 등록된 쿠키가 실제로 저장되어 있지 않다면 redirectPath로 페이지 이동
  serverCookieConfig: {
    cookieNames: ["SESSION", "SESSION-LOCAL"],
    redirectPath: "/login", // null로 설정시 리다이렉트 비활성화
  },

  // 인터셉터 콜백
  interceptors: {
    onRequest: (config) => {
      const token = getToken();
      if (token) {
        config.headers.Authorization = `Bearer${token}`;
      }
      return config;
    },
    onResponseError: (error) => {
      if (error.response?.status === 401) {
        window.location.href = "/login";
      }
      return Promise.reject(error);
    },
  },

  // 또는 setupInterceptors로 직접 제어 (기본 인터셉터 대체)
  // setupInterceptors: (instance) => {
  //   instance.interceptors.request.use(...);
  // },
});

프로젝트 구조 및 배포

본 프로젝트는 turborepo를 사용하여 모노레포로 구성되어있고, app 디렉토리 없이 packages로 구성되어있다.

모노레포를 안써도 되긴하지만 나중에 또 다른 라이브러리가 추가될 걸 염려해서 한번에 관리하려고 모노레포로 구성하였다.

pnpm build && pnpm pack 후 pnpm -r publish로 npm에 배포하였다.

깃허브: https://github.com/cher1shRXD/cher1shrxd-utils

후기

개인적인 생각으로 프론트엔드 개발자의 최종장이 npm 라이브러리 개발자인 것 같다.

(웹개발 10몇년 한 사람들 죄다 npm에 라이브러리 수두룩함)

더이상 보일러 플레이트를 복붙하지 않아도 된다는 해방감과

여러 프로젝트를 하며 모듈들을 변경하던 힘든 시절이 눈 앞에 스쳐 지나가지만 이젠 그럴 필요가 없다.

너무 행복하고

여러분도 라이브러리 한번씩 만들어 보세요. 노력 대비 만족감이 엄청나요. 👍

Image

훈수는 언제나 환영

수정

2026.01.01

api-client 라이브러리의 본체를 axios에서 fetch로 마이그레이션하고, .withISR(), withSSG() 메서드를 추가하여 Next.js를 더 잘 활용할 수 있도록 업데이트했다.

새 글 알림 받기

글이 마음에 드셨다면 블로그를 구독하고 새로운 소식을 받아보세요.

On this page