<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>myeongjae-kim.log</title>
        <link>https://velog.io/</link>
        <description>개발자</description>
        <lastBuildDate>Tue, 14 Jul 2026 07:23:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>myeongjae-kim.log</title>
            <url>https://velog.velcdn.com/images/myeongjae-kim/profile/490881fe-c3df-4e4d-b340-7989686d5a8d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. myeongjae-kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/myeongjae-kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Node.js 세상에서 타입-안전한 의존성 주입이 가능하다면: inversify-typesafe 라이브러리 소개]]></title>
            <link>https://velog.io/@myeongjae-kim/Node.js-%EC%84%B8%EC%83%81%EC%97%90%EC%84%9C-%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84%ED%95%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8B%A4%EB%A9%B4-inversify-typesafe-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@myeongjae-kim/Node.js-%EC%84%B8%EC%83%81%EC%97%90%EC%84%9C-%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84%ED%95%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8B%A4%EB%A9%B4-inversify-typesafe-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Tue, 14 Jul 2026 07:23:47 GMT</pubDate>
            <description><![CDATA[<p>요새 <a href="https://nextjs.org/">Next.js</a>와 <a href="tanstack.com/start">tanstack-start</a>로 상용 서비스의 백엔드 API 서버를 구현하고 운영할 수 있는지에 대한 실험을 하고 있습니다. 애플리케이션에 필요한 모든 인프라를 Serverless 환경으로 구성해서 인프라를 관리하는 비용을 극단적으로 줄이고 풀스택 프레임워크 하나로 프론트엔드와 백엔드를 통합해 조직의 생산성을 높일 수 있는지...</p>
<p>이미 기술적으로는 충분히 가능하다고 보이지만, Spring Boot와 AWS를 메인으로 사용하던 조직에 변화를 만들려면 기존 기술 스택의 만족스러운 점들을 최대한 잃지 않으면서도 새로운 기술 스택이 가져오는 장점이 충분히 크다는 것을 보여야 합니다.</p>
<p>백엔드의 비즈니스 로직을 IoC(Inversion of Control) 컨테이너로 관리하는 것도 그 중 하나입니다. Spring을 통해 유지하던 백엔드 비즈니스 로직의 낮은 복잡도를 Next.js를 사용하면서도 그대로 유지할 수 있을지 탐구중이고, 그 과정에서 Node.js 생태게의 IoC 컨테이너중 하나인 <a href="https://inversify.io/">inversify</a>의 타입 안전성을 강화할 수 있는 <a href="https://github.com/myeongjae-kim/inversify-typesafe">inversify-typesafe</a> 라이브러리를 작성해서 소개글을 적습니다.</p>
<hr>
<p>2026.06.02 추가) 이 라이브러리를 실전에 적용해본 후기입니다: <a href="https://myeongjae.kim/blog/2026/06/02/nextjs-inversify-typesafe-on-settlement-project">Next.js 풀스택 서버리스로 구현한 정산 데이터 전처리 시스템 개발 후기: inversify-typesafe로 프로젝트 복잡도 제어하기</a></p>
<h2 id="들어가며">들어가며</h2>
<p>2014년에 AWS Lambda가 등장하면서 Serverless 컴퓨팅이 산업적으로 주목받기 시작했습니다. 2018년에는 Amazon Aurora Serverless v1이 릴리즈되었고, 2021년 말에는 <a href="https://planetscale.com/">PlanetScale</a>처럼 개발자 경험을 극대화한 Serverless RDBMS(Relational Database Management System)가 등장해서 커뮤니티의 관심을 받았습니다.</p>
<p><a href="https://nextjs.org/">Next.js</a>는 등장부터 풀 스택 리액트 프레임워크로 주목을 받았고, 이제는 다양한 Serverless Edge Hosting 업체들(Vercel, Cloudflare, Netlify 등)을 통해서 풀 스택 리액트 애플리케이션을 간단하게 배포할 수 있는 세상이 되었습니다(<a href="https://opennext.js.org/">opennext</a>를 사용해야 하지만... 최근에는 플랫폼-중립적인 <a href="tanstack.com/start">tanstack-start</a>가 충분히 안정화가 되면서 상용 제품에 써도 큰 문제가 없을 것 같습니다).</p>
<p>Serverless RDBMS와 Serverless Edge Hosting은 이미 상용 제품에 활발이 사용되고 있습니다. 이 둘을 함께 활용하면 전형적인 웹 애플리케이션의 인프라에 필요한 노력을 극단적으로 줄일 수 있습니다. 예를 들어, 별도의 백엔드 API 서버 없이 Next.js의 백엔드에서 Serverless DB에 직접 커넥션을 맺고 데이터를 가져오는 것은 물론 <strong>Next.js의 백엔드를 그저 BFF(Backend for Frontend)가 아니라 백엔드 API를 제공하기 위한 프레임워크로도 사용할 수 있습니다</strong> (기술적으로 가능하지만 권장하지 않습니다. <a href="https://hono.dev/">Hono</a>같이 Serverless를 타겟으로 하는 프레임워크가 성능과 DX 모두 훨씬 낫습니다).</p>
<p>만약 상용 제품을 Next.js만 사용해서 구현한다면, 저장소 하나에 백엔드와 프론트엔드 코드를 같이 관리해야하므로 시스템의 복잡도는 기하급수로 올라가갑니다. 객체지향 세상에선 이미 IoC(Inversion of Control) 컨테이너가 복잡도를 통제하기 위한 표준적인 도구로 사용되고 있고 Node.js 생태계에도 <a href="https://nestjs.com/">NestJS</a>나 <a href="https://inversify.io/">InversifyJS</a>같은 훌륭한 IoC 컨테이너 라이브러리가 존재합니다</p>
<p>웹 관련 기능은 Next.js가 담당할 것이기 때문에 이 상황에서는 다른 부가 기능 없이 단순히 IoC 기능만 제공할 수 있는 컨테이너만 있으면 됩니다. NestJS에서 웹 부분을 제외하고 DI 기능만 쓸 수 있지만(<a href="https://docs.nestjs.com/faq/serverless#using-standalone-application-feature">https://docs.nestjs.com/faq/serverless#using-standalone-application-feature</a>), 번들 크기가 inversify보다 3배 무겁고 (<a href="https://bundlephobia.com/package/inversify@7.10.8">inversify: 57.9kB</a>, <a href="https://bundlephobia.com/package/@nestjs/core@11.1.10">@nestjs/core: 189.2kB</a>) 제게 필요했던 기능이 Inversify만으로 충분히 제공되기 때문에 Next.js로 구성한 백엔드의 복잡도를 통제하기 위한 도구로 inversify를 골라 이것저것 실험을 해보고 있습니다.</p>
<p>Inversify는 그 자체로 이미 훌륭하지만, 사용해보니 타입 선언을 조금만 개선하면 타입스크립트의 기능을 활용해 타입-안전하게 의존성 주입을 할 수 있을 것 같아서 라이브러리를 작성했습니다: <a href="https://github.com/myeongjae-kim/inversify-typesafe">inversify-typesafe</a></p>
<h2 id="inversify의-기본-예제-살펴보기">inversify의 기본 예제 살펴보기</h2>
<p>아래는 inversify가 제공하는 <a href="https://inversify.io/docs/introduction/dependency-inversion/">Dependency Inversion 예제</a>입니다.</p>
<pre><code class="language-ts">import { Container, inject, injectable, ServiceIdentifier } from &#39;inversify&#39;;

interface Weapon {
  damage: number;
}

// symbol은 Node.js의 원시타입중 하나. 이 예제에서는 문자열과 유사하다고 생각해도 충분하다.
const ninjaServiceId: ServiceIdentifier&lt;Ninja&gt; = Symbol.for(&#39;NinjaServiceId&#39;);
const weaponServiceId: ServiceIdentifier&lt;Weapon&gt; = Symbol.for(&#39;WeaponServiceId&#39;);

@injectable()
class Katana implements Weapon {
  public readonly damage: number = 10;
}

@injectable()
class Ninja {
  constructor(
    // JVM에서는 런타임에도 타입 정보가 살아있으므로, 어떤 인터페이스의 구현체가 인스턴스가 하나만 등록되어 있다면
    // 컴포넌트의 인스턴스를 주입받기 위해 따로 이름을 입력하지 않아도 인스턴스 주입이 가능하다.
    // 그러나 타입스크립트는 런타임에 타입 정보가 사라지므로 어떤 인터페이스의 구현체 인스턴스가 하나만
    // 등록되어 있더라도 주입받을 인스턴스에 대한 이름(혹은 ID)을 필수로 입력해야 한다.
    // 이 예제에서는 런타임에 Weapon 인스턴스의 타입 정보를 사용할 수 없으므로 weaponServiceId라는 값을 사용한다.
    @inject(weaponServiceId)
    public readonly weapon: Weapon, // 구현체 대신 인터페이스를 의존 (Dependency Inversion)
  ) {}
}

const container: Container = new Container();

// 서비스 ID와 구현체를 매핑
container.bind(ninjaServiceId).to(Ninja);
container.bind(weaponServiceId).to(Katana);

// 특정 bean을 요청했을 때 인스턴스를 생성해서 제공한다 (lazy).
const ninja: Ninja = container.get(ninjaServiceId);

console.log(ninja.weapon.damage);</code></pre>
<p>(Demo: <a href="https://stackblitz.com/edit/inversify-di?file=src%2Fmain.ts">https://stackblitz.com/edit/inversify-di</a>)</p>
<p>이대로도 나쁘지 않지만, 타입스크립트를 더 잘 활용하면 더 나은 코드를 작성할 수 있지 않을까요?</p>
<p>예를 들어</p>
<ol>
<li>컨테이너에서 서비스를 조회할 때 서비스 ID만 입력해도 자동으로 타입이 추론된다면?</li>
<li>컨테이너에 등록되지 않은 서비스 ID를 입력했을 때 컴파일 에러가 발생한다면?</li>
<li>서비스를 주입받을 때 등록되지 않은 서비스 ID를 입력하면 컴파일 에러가 발생한다면?</li>
</ol>
<h2 id="inversify-typesafe-라이브러리를-작성하다">inversify-typesafe 라이브러리를 작성하다</h2>
<p>Inversify 컨테이너의 타입-안전성을 강화하기 위해 간단한 라이브러리를 작성했습니다: <a href="https://github.com/myeongjae-kim/inversify-typesafe">inversify-typesafe</a></p>
<p>실행 가능한 Demo: <a href="https://stackblitz.com/edit/inversify-typesafe?file=test%2Fmain.test.ts">https://stackblitz.com/edit/inversify-typesafe</a></p>
<pre><code class="language-ts">import { createTypesafeContainer, returnTypesafeInject, TypesafeServiceConfig } from &quot;inversify-typesafe&quot;;

// https://inversify.io/docs/introduction/dependency-inversion/
interface Weapon {
  damage: number;
}

class Katana implements Weapon {
  public readonly damage: number = 10;
}

export const typesafeInject = returnTypesafeInject&lt;Services&gt;()

class Ninja {
  constructor(
    // decorator의 parameter가 존재하지 않는 서비스 ID면 컴파일에러 발생
    @typesafeInject(&quot;weaponServiceId&quot;)
    public readonly weapon: Weapon,
  ) { }
}

export type Services = {
  &quot;ninjaServiceId&quot;: Ninja; // class
  &quot;weaponServiceId&quot;: Weapon; // interface
};

export const serviceConfig: TypesafeServiceConfig&lt;Services&gt; = {
  // &#39;Ninja&#39;와 호환되지 않는 서비스를 바인딩하려고 하면 컴파일 에러 발생
  &quot;ninjaServiceId&quot;: (bind) =&gt; bind().to(Ninja),
  // &#39;Weapon&#39;과 호환되지 않는 서비스를 바인딩하려고 하면 컴파일 에러 발생
  // 두 번째 파라미터인 _container를 활용해서 복잡한 바인딩을 수행할 수 있다
  &quot;weaponServiceId&quot;: (bind, _container) =&gt; bind().to(Katana),
};

// 컨테이너를 생성할 때 타입 정보를 따로 입력하지 않아도 추론해서 사용한다
const typesafeContainer = createTypesafeContainer(serviceConfig);

// 서비스 ID를 입력하면 자동으로 타입 추론을 한다.
console.log(typesafeContainer.get(&quot;ninjaServiceId&quot;).weapon.damage);</code></pre>
<h3 id="구현-철학">구현 철학</h3>
<p><code>inversify-typesafe</code>를 구현하면서 다음과 같은 점들을 고려했습니다:</p>
<ol>
<li>라이브러리는 타입-안전성을 위해 특정한 서비스 등록 방식을 강제하지만, InversifyJS의 기능을 제한해서는 안 된다.</li>
<li>라이브러리 사용자는 원할 때 언제든지 InversifyJS의 모든 기능을 사용할 수 있어야 한다.</li>
<li>위 두 가지 원칙을 지키면서 사용자의 실수를 가능한 한 컴파일 타임에 잡아야 한다.</li>
<li>라이브러리 사용자가 선언해야 하는 타입은 최소화해야 한다.</li>
</ol>
<h3 id="장점">장점</h3>
<ul>
<li>컨테이너에 문자열 서비스 ID를 입력하면 추가적인 타입 작성 없이 등록된 타입을 자동으로 추론합니다.</li>
<li>서비스를 찾기 위해 <code>get</code> 메서드에 문자열을 입력할 때, 코드 편집기의 자동 완성을 통해 등록된 서비스 ID를 확인할 수 있습니다.</li>
<li>컨테이너에 등록되지 않은 서비스 ID를 입력하면 컴파일 에러가 발생합니다.</li>
<li>서비스를 주입할 때 등록되지 않은 서비스 ID를 입력하면 컴파일 에러가 발생합니다.</li>
<li>추가적인 peer depepdency가 없습니다. 오직 <code>inversify</code>와 <code>reflect-metadata</code>만 있으면 됩니다.</li>
<li>100% 테스트 커버리지.</li>
</ul>
<h3 id="사용법">사용법</h3>
<h4 id="1-services-맵-타입-선언">1. <code>Services</code> 맵 타입 선언</h4>
<pre><code class="language-ts">export type Services = {
  &quot;ninjaServiceId&quot;: Ninja; // class
  &quot;weaponServiceId&quot;: Weapon; // interface
};</code></pre>
<p>사용자는 컨테이너에 등록될 서비스를 선언하기 위해 <code>Services</code>(또는 원하는 이름) 맵 타입을 작성해야 합니다. <code>Services</code> 타입의 키는 서비스 ID로 사용됩니다. InversifyJS는 다양한 타입(<code>class</code>, <code>symbol</code> 등)을 서비스 ID로 등록하지만, <code>inversify-typesafe</code>는 오직 <code>string</code> 타입만을 서비스 ID로 사용합니다. <code>string</code>을 사용함으로써 <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types">TypeScript의 String Literal Types</a> 기능을 활용하여 마법 같은 타입 안전성을 제공할 수 있습니다.</p>
<h4 id="2-서비스-설정-작성">2. 서비스 설정 작성</h4>
<pre><code class="language-ts">import { TypesafeServiceConfig } from &quot;inversify-typesafe&quot;;

export const serviceConfig: TypesafeServiceConfig&lt;Services&gt; = {
  &quot;ninjaServiceId&quot;: (bind) =&gt; bind().to(Ninja),
  &quot;weaponServiceId&quot;: (bind, _container) =&gt; bind().to(Katana),
};</code></pre>
<p>이 라이브러리는 <code>TypesafeServiceConfig&lt;T&gt;</code>라는 유틸리티 타입을 제공합니다. <code>TypesafeServiceConfig&lt;T&gt;</code>는 타입 <code>T</code>의 키를 서비스 ID로 사용하도록 강제합니다. 앞서 선언한 <code>Services</code> 타입을 <code>TypesafeServiceConfig</code>의 타입 매개변수로 전달하면 <code>Services</code> 타입의 키만 사용할 수 있도록 제한됩니다. <code>Services</code>에 존재하지 않는 키를 입력하거나 <code>Services</code>의 모든 키에 대한 함수를 제공하지 않으면 컴파일 에러가 발생하여 사용자가 더 안전하게 코드를 작성할 수 있게 합니다.</p>
<p>객체의 값은 사용자가 Inversify의 모든 바인딩 기능을 활용할 수 있도록 람다를 사용합니다. 람다는 첫 번째 매개변수로 <code>bind</code>, 두 번째 매개변수로 <code>container</code>를 받습니다. 첫 번째 매개변수 <code>bind</code>는 서비스 ID를 컨테이너에 바인딩하기 위한 <a href="https://en.wikipedia.org/wiki/Thunk">thunk</a> <code>() =&gt; container.bind(serviceId)</code>입니다. 객체의 키를 서비스 ID로 바인딩하는 thunk가 매개변수로 전달되므로, 사용자는 서비스 ID에 서비스를 어떻게 매핑할지 선택할 수 있습니다(<code>bind().to(Service)</code>). 사용자가 <code>Services</code> 타입 선언과 호환되지 않는 서비스를 매핑하려고 하면 컴파일 에러가 발생합니다.</p>
<img src="https://cdn.myeongjae.kim/blog/2026/01/autocomplete.webp" width="707" />

<p>람다의 두 번째 매개변수는 <code>container</code>를 받습니다. 간단한 경우에는 첫 번째 매개변수 <code>bind</code>만 사용해도 충분하지만, 서비스 등록 과정에서 컨테이너에 직접 접근해야 하는 경우 두 번째 매개변수를 활용할 수 있습니다.</p>
<h4 id="3-타입-안전한-컨테이너-생성">3. 타입 안전한 컨테이너 생성</h4>
<pre><code class="language-ts">import { createTypesafeContainer } from &quot;inversify-typesafe&quot;;

const typesafeContainer = createTypesafeContainer(serviceConfig);</code></pre>
<p><code>createTypesafeContainer()</code> 함수는 <code>TypesafeServiceConfig&lt;T&gt;</code> 타입의 인수를 받아 <code>TypesafeContainer&lt;T&gt;</code>를 반환합니다. 사용자는 제네릭 타입 매개변수를 수동으로 지정할 필요가 없습니다.</p>
<h4 id="4-서비스-가져오기">4. 서비스 가져오기</h4>
<pre><code class="language-ts">const ninjaService = typesafeContainer.get(&quot;ninjaServiceId&quot;);</code></pre>
<p>사용자가 선언한 <code>Services</code> 타입의 키를 인수로 전달하면 바인딩된 서비스가 반환됩니다. 반환 타입은 추가적인 타입 작성 없이 잘 추론되며, 코드 편집기의 자동 완성을 통해 등록된 서비스 ID를 확인할 수 있습니다.</p>
<img src="https://cdn.myeongjae.kim/blog/2026/01/autocomplete.webp" alt="" width="626" />

<p><code>Services</code>의 키가 아닌 값을 입력하면 컴파일 에러가 발생합니다.</p>
<img src="https://cdn.myeongjae.kim/blog/2026/01/get-compile-error.webp" alt="" width="955" />

<h4 id="5-서비스-주입하기">5. 서비스 주입하기</h4>
<pre><code class="language-ts">import { returnTypesafeInject } from &quot;inversify-typesafe&quot;;

export const typesafeInject = returnTypesafeInject&lt;Services&gt;()

class Ninja {
  constructor(
    @typesafeInject(&quot;weaponServiceId&quot;)
    public readonly weapon: Weapon,
  ) { }
}</code></pre>
<p>사용자가 선언한 <code>Services</code> 타입을 타입 매개변수로 하여 고계 함수 <code>returnTypesafeInject&lt;T&gt;()</code>를 호출하면 데코레이터 함수가 반환됩니다. 이 데코레이터의 매개변수에 <code>Services</code> 타입의 키가 아닌 값을 입력하면 컴파일 에러가 발생합니다.</p>
<img src="https://cdn.myeongjae.kim/blog/2026/01/inject-compile-error.webp" alt="" width="924" />

<h4 id="6-모든-inversifyjs-기능-사용-가능">6. 모든 InversifyJS 기능 사용 가능</h4>
<pre><code class="language-ts">import { ServiceIdentifier } from &quot;inversify&quot;;

const ninjaService = typesafeContainer._get(&quot;ninjaServiceId&quot; as ServiceIdentifier&lt;Ninja&gt;);</code></pre>
<p><code>TypesafeContainer&lt;T&gt;</code> 타입의 <code>_get</code> 메서드는 기존 <code>Container</code> 타입의 <code>get</code> 메서드와 동일한 기능을 수행합니다. 타입 안전성이 강화된 기능이 아닌 원래의 <code>get</code> 메서드가 필요할 때 <code>_get</code> 메서드를 사용할 수 있습니다.</p>
<p><code>get</code> 메서드를 제외한 모든 메서드는 InversifyJS <code>Container</code> 타입과 동일합니다.</p>
<h2 id="조금-더-spring과-비슷하게-inversify-typesafe-spring-like">조금 더 Spring과 비슷하게: inversify-typesafe-spring-like</h2>
<p>InversifyJS의 scope 기본값은 <code>Request</code>입니다. 컨테이너의 <code>get</code> 함수를 호출할 때마다 새로운 객체를 생성합니다.</p>
<p>웹 백엔드 환경에서는 특별한 경우가 아니라면 한 번 생성한 인스턴스를 재활용하므로 기본 scope 값을 <code>Singleton</code>으로 사용하는게 보통이고, Spring의 IoC 컨테이너의 기본 scope도 <code>Singleton</code>입니다.</p>
<p><a href="https://github.com/myeongjae-kim/inversify-typesafe/tree/main/packages/inversify-typesafe-spring-like">inversify-typesafe-spring-like</a> 라이브러리는 inversify-typesafe 라이브러리를 확장해서 Spring Framework와 유사한 DX(Developer Experience)를 제공하는 간단한 라이브러리입니다.</p>
<p>라이브러리의 API는 Spring의 용어를 반영하여 설계했습니다:</p>
<ol>
<li><code>createTypesafeContext()</code> -&gt; <code>ApplicationContext()</code><ul>
<li>Note: <code>defaultScope</code>는 기본적으로 <code>Singleton</code>으로 설정됩니다.</li>
</ul>
</li>
<li><code>returnTypesafeInject()</code> -&gt; <code>returnAutowired()</code></li>
<li><code>TypesafeServiceConfig&lt;T&gt;</code> -&gt; <code>BeanConfig&lt;T&gt;</code></li>
</ol>
<p>기본 scope와 용어 변경 외에는 inversify-typesafe와 동일합니다.</p>
<p>실행 가능한 Demo: <a href="https://stackblitz.com/edit/inversify-typesafe-spring-like?file=test%2Fmain.test.ts">https://stackblitz.com/edit/inversify-typesafe-spring-like</a></p>
<pre><code class="language-ts">import { ApplicationContext, BeanConfig, returnAutowired } from &quot;inversify-typesafe-spring-like&quot;;

interface Article {
  id: number;
  title: string;
  content: string;
}

interface ArticleOutgoingPort {
  getById(id: number): Promise&lt;Article&gt;
}

class ArticleRepository implements ArticleOutgoingPort {
  getById(id: number): Promise&lt;Article&gt; {
    return Promise.resolve({
      id: id,
      title: `title #${id}`,
      content: `content #${id}`,
    })
  }
}

interface GetArticleUseCase {
  execute(id: number): Promise&lt;Article&gt;
}

const { Autowired } = returnAutowired&lt;Beans&gt;();

class ArticleQueryService implements GetArticleUseCase {
  constructor(
    @Autowired(&quot;ArticleOutgoingPort&quot;) // compile error if a parameter of @Autowired is not a key of Beans.
    private readonly articleOutgoingPort: ArticleOutgoingPort,
  ) { }
  execute(id: number): Promise&lt;Article&gt; {
    return this.articleOutgoingPort.getById(id);
  }
}

type Beans = {
  GetArticleUseCase: GetArticleUseCase; // interface (class is also possible)
  ArticleOutgoingPort: ArticleOutgoingPort; // interface (class is also possible)
}

const beanConfig: BeanConfig&lt;Beans&gt; = {
  // compile error if ArticleQueryService is not compatible with GetArticleUseCase.
  GetArticleUseCase: (bind) =&gt; bind().to(ArticleQueryService),
  // compile error if ArticleRepository is not compatible with ArticleOutgoingPort.
  ArticleOutgoingPort: (bind) =&gt; bind().to(ArticleRepository), 
}

const applicationContext = ApplicationContext(beanConfig);

const getArticleUseCase = applicationContext.get(&quot;GetArticleUseCase&quot;)

getArticleUseCase.execute(1).then(console.log)</code></pre>
<h2 id="마무리">마무리</h2>
<p>제 개인 블로그(<a href="https://myeongjae.kim/)%EC%99%80">https://myeongjae.kim/)와</a> 회사의 프로젝트에서 <a href="https://www.npmjs.com/package/inversify-typesafe">inverisfy-typesafe</a>를 적극적으로 사용하고 있습니다. 최근에는 연 1,400억 원 규모의 매출을 올리는 국내 콘텐츠 회사의 정산 데이터 전처리 시스템을 Next.js로 구현하면서 이 라이브러리를 활용해 백엔드의 복잡도를 성공적으로 통제했습니다.</p>
<p>개인 블로그의 프레임워크를 <a href="https://tanstack.com/start">tanstack-start</a>로 교체하면서도, inversify로 보호하던 core 레이어는 코드 변경 없이 프레임워크 교체를 완료했습니다.</p>
<p>타입스크립트와 서버리스 환경의 장점을 취하면서도 의존성 주입을 활용하고 싶으신 분들에게 적극 추천 드립니다.</p>
<ul>
<li><a href="https://www.npmjs.com/package/inversify-typesafe">inversify-typesafe</a></li>
<li><a href="https://www.npmjs.com/package/inversify-typesafe-spring-like">inversify-typesafe-spring-like</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js v13 App Router로 제품 만들기: 이제 더 이상 Axios를 쓰지 않기로 했습니다]]></title>
            <link>https://velog.io/@myeongjae-kim/Next.js-v13-App-Router%EB%A1%9C-%EC%A0%9C%ED%92%88-%EB%A7%8C%EB%93%A4%EA%B8%B0-fetch%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@myeongjae-kim/Next.js-v13-App-Router%EB%A1%9C-%EC%A0%9C%ED%92%88-%EB%A7%8C%EB%93%A4%EA%B8%B0-fetch%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Mon, 07 Aug 2023 11:46:16 GMT</pubDate>
            <description><![CDATA[<p>회사 기술블로그에 기고한 글입니다: <a href="https://blog.deering.co/next-js-app-router-and-fetch-library/">https://blog.deering.co/next-js-app-router-and-fetch-library/</a></p>
<hr>

<p>안녕하세요, 디어코퍼레이션의 김명재입니다.</p>
<p>토이프로젝트가 아닌 실제 제품에 Next.js App Router를 사용해보셨나요? 이번에 저희 회사에서 웹 애플리케이션을 새로 셋팅할 필요가 생겼고 저는 과감하게 Next.js의 App Router를 선택했습니다(회사에서는 기존 제품에도 Next.js를 사용하고 있었고 저도 5년째 사용하고 있습니다). 이 글에서는 App Router를 선택한 배경과 App Router에서 fetch를 사용하기 위해 어떻게 정든 Axios를 떠나보냈는지를 중심 주제로 다룹니다.</p>
<h2 id="nextjs-app-router와-fetch">Next.js App Router와 <code>fetch</code></h2>
<p>Next.js는 <a href="https://nextjs.org/blog/next-13#new-app-directory-beta">2022년 10월 25일 컨퍼런스에서 app directory를 발표</a>했습니다. App directory는 기존의 Page Router를 대체합니다. 2023년 5월 4일 Next.js 13.4.0버전에서 app direoctry는 beta딱지를 벗고 stable이 되었습니다. 저희 회사에서 도입한 시점인 7월달엔 13.4.6버전이 릴리즈되어 있었고, stable 변경 이후 6번의 업데이트가 있었으니 이제 제품에 써봐도 괜찮을 것 같다는 느낌이었습니다(<a href="https://nextjs.org/blog/next-13-2">App directory는 13.2버전부터 App Router라는 이름으로 불리기 시작했습니다</a>. 이하에서는 정식 명칭인 App Router로 적겠습니다).</p>
<p>그러나 느낌만으로 결정을 할 수는 없으니... 제품에 적용하기 전에 먼저 제가 9년째 유지보수하고 있는 <a href="https://myeongjae.kim">제 블로그</a>를 App Router로 다시 구현하면서 학습을 했습니다. 디렉토리 구성, 서버 사이드 렌더링, 비동기 호출(API 요청), 에러처리, 캐시 등 실제 제품을 구현할 때 고려해야 할 것들을 신경쓰며 학습을 했습니다. App Router를 사용한 블로그의 모든 기능이 문제없이 작동했고 App Router에 익숙해지면서 자신감이 생겼습니다.</p>
<p>App Router를 사용하면서 가장 강하게 들었던 느낌은 AppRouter가 Page Router보다 훨씬 더 서버 친화적으로 코드를 작성하도록 유도한다는 것이었습니다. 이전엔 클라이언트 렌더링을 위한 프레임워크에 서버쪽 기능을 추가한 느낌이었다면, App Router부터는 렌더링의 주도권이 완전히 서버쪽으로 넘어갔다는 느낌을 받았습니다. 옛날의 클래식한 웹 프레임워크의 향수가 느껴지는 그런 느낌. 하지만 모던한.</p>
<p>App Router 문서를 읽어보니 <a href="https://nextjs.org/docs/app/api-reference/functions/fetch">Next.js 서버쪽에서 캐싱 관련 기능을 <code>fetch</code>를 확장해서 제공</a>하고 있고, <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries"><code>fetch</code>가 아닌 다른 라이브러리를 사용한다면 직접 캐싱 관련 설정을 해주어야 합니다</a>.</p>
<p>현재 회사는 물론 일해왔던 회사들에서도 제품에 <code>fetch</code>가 아닌 Axios를 사용했었습니다. 지금까지 개발했던 제품들이 Axios의 번들 크기(<a href="https://bundlephobia.com/package/axios">11.2KB, minified + gzipped</a>)도 허용하지 못할만큼 용량 최적화가 필요하진 않았기 때문입니다. 익숙한 Axios를 사용해서 빠르게 개발하는게 이제까지는 좋은 선택이었지만, 프레임워크에서 <code>fetch</code>를 쓰라고 하니 드디어 미루고 미뤘던 <code>fetch</code>를 학습해야 할 때가 온 것 같았습니다. Next.js에서 제공하는 <code>fetch</code>를 거부하고 굳이 Axios를 고집하면서 Next.js에 캐시 관련 설정을 수동으로 관리할 명분도 없었습니다.</p>
<h2 id="fetch는-이런저런게-아쉬워요"><code>fetch</code>는 이런저런게 아쉬워요!</h2>
<p>아래 항목들은 Axios에서 제공하지만 <code>fetch</code>에는 없어서 아쉽다고 느꼈던 기능들입니다.</p>
<ol>
<li>baseUrl을 설정할 수 없다.</li>
<li>default header를 설정할 수 없다.</li>
<li>request, response interceptor가 없다.</li>
<li>json serialize, deserialize를 직접 해야 한다.</li>
</ol>
<p>Axios는 인스턴스를 생성할 때 baseUrl과 default header를 설정하면 모든 요청에 대해서 url의 origin이 비어있는 경우 baseUrl을 origin으로 사용하고 default header를 넣어줍니다.</p>
<pre><code class="language-ts">import axios from &#39;axios&#39;;

const axiosInstance = axios.create({
    baseURL: &#39;https://your-api-endpoint.com&#39;
    headers: {
        &#39;Accept&#39;: &#39;application/json&#39;,
    }
})

// https://your-api-endpoint.com/article 에
//  &#39;Accept: application/json&#39; 헤더가 붙어있는 get요청을 보낸다
axios.get(&#39;/article&#39;)</code></pre>
<p>request interceptor는 요청을 보내기 전에, response interceptor는 요청을 받은 후에 실행합니다.</p>
<pre><code class="language-ts">axiosInstance.interceptors.request.use(config =&gt; {
  // 매 요청마다 localStorgae의 토큰을 조회해서 헤더에 추가한다.
  config[&#39;Authorization&#39;] = localStorage.getItem(&#39;token&#39;);
  return config;
})

axiosInstance.interceptors.request.use(undefined, error =&gt; {
  // Unauthorized 응답을 받으면 가지고 있던 토큰을 제거한다.
  if (error.isAxiosError &amp;&amp; e.response?.status === 401) {
    localStorage.removeItem(&#39;token&#39;);
  }
})</code></pre>
<p>Axios는 request config의 data에 object를 넣을 수 있고, response도 json이라면 자동으로 object로 만들어줍니다.</p>
<pre><code class="language-ts">axiosInstance.post&lt;{id: number}&gt;(&#39;/create&#39;, {
  title: &#39;title&#39;,
  content: &#39;content&#39;
})
  .then(response =&gt; response.data)
  .then(console.log)

// 출력 예시: {id: 1}. 문자열이 아니라 객체가 나온다.</code></pre>
<p>Axios에 다른 여러가지 기능들도 있지만, 특히 이 4가지 기능이 없다면 모든 요청에 매번 같은 설정값을 넣어줘야 합니다. 변경사항이 생기기라도 하면 <code>fetch</code>함수를 호출할 때마다 같은 작업을 해야 하고, 혹시 뭔가를 빠뜨린다면 장애로 이어질 수도 있습니다.</p>
<h3 id="누가-만들어놓지-않았을까">누가 만들어놓지 않았을까?</h3>
<p>저 혼자 Next.js를 쓰는 것도 아니고 저 혼자 <code>fetch</code>를 쓰는 것도 아니니 당연히 누군가 위 기능을 제공하는 라이브러리를 만들었을텐데요, 아래는 제가 찾은 솔루션들과 장단점들입니다.</p>
<h4 id="1-fetch-intercept">1. fetch-intercept</h4>
<p><a href="https://www.npmjs.com/package/fetch-intercept">https://www.npmjs.com/package/fetch-intercept</a></p>
<p>fetch와 interceptor관련 내용으로 검색을 하면 가장 먼저 찾을 수 있는 라이브러리입니다. 하지만 baseUrl과 default headers 기능을 제공하지 않고 전역에 선언되어있는 <code>fetch</code>를 몽키패치 해버리기 때문에 이 라이브러리를 선택할 수는 없었습니다.</p>
<h5 id="nextjs에서-fetch를-사용하는-3가지-맥락">Next.js에서 <code>fetch</code>를 사용하는 3가지 맥락</h5>
<p>Next.js에서는 <code>fetch</code>를 다양한 맥락에서 사용하게 됩니다. 제 멘탈 모델에서는 크게 3가지가 있습니다.</p>
<ol>
<li>클라이언트 사이드(웹브라우저)에서 AJAX 요청</li>
<li>서버 사이드에서 렌더링하기 위한 AJAX 요청</li>
<li>서버 사이드에서 렌더링이 아니라 프록시 역할로 AJAX 요청을 하고 응답을 그대로 내보냄(<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route">route.ts 파일</a>)</li>
</ol>
<p>하나의 <code>fetch</code>에서 이 3가지 상황을 모두 대응하는건 좋은 선택이 아니므로 fetch-intercept는 기각입니다.</p>
<h5 id="장점">장점</h5>
<ul>
<li>가벼운 번들 사이즈 (762 Byte, minified + gzipped)</li>
</ul>
<h5 id="단점">단점</h5>
<ul>
<li>baseUrl 기능이 없다.</li>
<li>default headers 기능이 없다.</li>
<li>전역에 선언된  <code>fetch</code> 에 몽키패치를 해버리기 때문에 모든 <code>fetch</code> 호출에 request, response interceptor가 적용된다. <code>fetch</code>를 다양한 맥락에서 사용해야 하는 Next.js에는 어울리지 않는다.</li>
</ul>
<h4 id="2-wretch">2. wretch</h4>
<p><a href="https://github.com/elbywan/wretch">https://github.com/elbywan/wretch</a></p>
<p>wretch는 fetch-intercept보다 많은 기능을 제공합니다. baseUrl과 default header도 설정할 수 있습니다. interceptor도 middleware라는 이름으로 제공하고 있습니다. 전역 <code>fetch</code>를 건드리지 않고 wretch instance를 생성해서 사용하는 방식이기 때문에 side effect도 없어서 안심하고 사용할 수 있습니다.</p>
<p>하지만 Axios처럼 400이상의 응답 상태에 대해서 모두 에러를 던지기 때문에 Next.js 서버쪽에서 프록시 역할을 할 때 wretch는 적합하지 않습니다. 외부 서비스 API를 호출하고 응답을 그대로 반환하는 프록시 역할에 wretch를 사용한다면, 400이상의 응답 상태 때문에 던진 예외를 다시 잡아서 정상적인 Response 객체로 만들어서 return해야 하는데 그럴거면 그냥 원본의 <code>fetch</code>를 사용하는게 낫습니다.</p>
<pre><code class="language-ts">// file: src/app/sample/api/[[...path]]/route.ts
// Next.js의 route.ts에서 fetch로 외부 서비스를 호출한다.
// 상태 코드에 따른 예외를 던지지 않기 때문에 Response를 그대로 클라이언트에 전달하기만 하면 된다.

import { NextRequest } from &quot;next/server&quot;;  

const pathPrefix = &quot;/sample/api&quot;;

export async function GET(request: NextRequest) {  
  const { nextUrl, method, headers } = request;  

  return fetch(&quot;https://postman-echo.com&quot; + nextUrl.pathname.replace(pathPrefix, &quot;&quot;), {  
    method,  
    headers,  
  });  
}</code></pre>
<p>게다가 wretch는 <code>fetch</code>와 호환되지 않는 자신만의 인터페이스를 가지고 있기 때문에 wretch 인스턴스를 생성할 때 뿐만 아니라 사용할 때도 wretch만의 인터페이스를 학습해야 한다는 점도 아쉬웠습니다. 위에서 이야기한 <a href="#nextjs%EC%97%90%EC%84%9C-fetch%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-3%EA%B0%80%EC%A7%80-%EB%A7%A5%EB%9D%BD">Next.js의 3가지 맥락</a>을 하나의 라이브러리로 통합하지 못하고 파편화되는건 만족스럽지 않기 때문에 wretch도 선택할 수 없었습니다.</p>
<h5 id="장점-1">장점</h5>
<ul>
<li>다양한 기능 (baseUrl, default header, interceptor 모두 지원)</li>
<li>상대적으로 가벼운 번들 사이즈 (1.9KB, minified + gzipped)</li>
</ul>
<h5 id="단점-1">단점</h5>
<ul>
<li>Axios처럼 response status에 따라 에러를 던지기 때문에 Next.js의 route.ts에서 사용하려면 추가 조치가 필요하다. 그럴바엔 그냥 <code>fetch</code>를 쓰는게 낫다.</li>
<li>wretch 인스턴스를 생성할 때 뿐만 아니라 사용할 때도 <code>fetch</code>와 호환되지 않는 자신만의 인터페이스를 가지고 있기 때문에 추가로 학습이 필요하다.</li>
</ul>
<h4 id="3-ofetch">3. ofetch</h4>
<p><a href="https://github.com/unjs/ofetch">https://github.com/unjs/ofetch</a></p>
<p>ofetch는 <a href="https://nuxt.com/docs/api/utils/dollarfetch">Nuxt.js에서 기본으로 사용하는 라이브러리</a>입니다. wretch와 마찬가지로 다양한 기능을 제공하고 있기 때문에 기능만으로는 부족함이 없었고, 상태 코드에 따른 에러를 던지지 않게 설정할 수 있는 옵션도 제공하고 있습니다(<a href="https://github.com/unjs/ofetch/issues/207">https://github.com/unjs/ofetch/issues/207</a>). wretch와는 다르게 자신만의 인터페이스를 정의한게 아니라 <code>fetch</code> 인터페이스의 호환을 유지하면서 추가 기능을 붙였기 때문에 wretch보다는 필요한 학습량도 적고, proxy역할을 할 때도 특별한 변환작업 없이 사용할 수 있습니다.</p>
<p>하지만 검색해봐도 ofetch를 Next.js의 App Router에서 사용해봤다는 레퍼런스가 없었기 때문에 만약 ofetch를 선택했더라면 Next.js와 잘 맞을지, 그리고 문제가 생겼을 때 트러블슈팅을 직접 해야한다는 것도 부담이었습니다.</p>
<p>번들 사이즈도 fetch-intercept, wretch에 비해 많이 컸습니다 (5KB). 제가 필요한건 그저 baseUrl과 default header 설정, 그리고 interceptor인데... 번들 사이즈는 큰 이슈는 아니었지만 그래도 아쉽긴 마찬가지였습니다.</p>
<h5 id="장점-2">장점</h5>
<ul>
<li>다양한 기능 (baseUrl, default header, interceptor 모두 지원)</li>
<li>Next.js의 route.ts파일에서도 사용할 수 있도록 response 상태와 상관 없이 예외를 던지지 않고 응답을 전달할 수 있다.</li>
<li>fetch와 호환되는 인터페이스를 가졌다.</li>
</ul>
<h5 id="단점-2">단점</h5>
<ul>
<li>Next.js에서 <code>ofetch</code>를 사용하는 레퍼런스가 부족하다. App Router도 새로운 기술인데 여기에 ofetch까지 얹어서 사용하기는 부담스럽다.</li>
<li>필요한 기능에 비해 번들 사이즈가 크다 (5KB)</li>
</ul>
<h3 id="쓸만한-라이브러리가-없다">쓸만한 라이브러리가 없다!</h3>
<p>도저히 마음에 드는 라이브러리가 없어서 그냥 직접 만들었습니다(??)</p>
<h2 id="사실은-라이브러리를-홍보하기-위해-작성한-글입니다">사실은 라이브러리를 홍보하기 위해 작성한 글입니다</h2>
<p>제가 원하던 기능은 딱 3가지(baseUrl, default headers, interceptors)였고 Next.js에서 제공하는 <code>fetch</code> 구현체를 사용할 수 있어야 했습니다. <code>fetch</code>관련 라이브러리중에 그나마 찾을 수 있었던게 위 3개의 라이브러리였고 모두 만족스럽지 않아서 그냥 직접 구현해서 쓰기로 했습니다. 복잡한 기능이 필요한게 아니었고, 처음부터 외부에 공개할 라이브러리를 만들려고 한 것도 아니었기 때문에 빠르게 만들었습니다.</p>
<h3 id="라이브러리로-공개할-예정에-없던-코드">라이브러리로 공개할 예정에 없던 코드</h3>
<p><img src="https://cdn.myeongjae.kim/blog/2023/08/fetch-1.png" alt=""></p>
<p>(fyi. 저희 회사는 반말을 사용하고 있습니다 😄 <a href="https://notion.deering.co/">https://notion.deering.co/</a>)</p>
<p><img src="https://cdn.myeongjae.kim/blog/2023/08/fetch-2.png" alt=""></p>
<p>(스크린샷의 주석 2번 항목은 잘못 적혀있습니다. &#39;generic으로 response body type을 정하는 기능&#39;도 함수 안에서 구현하려고 하다가 바깥에서 처리하도록 결정했는데 PR올릴 때는 주석이 아직 남아있었네요)</p>
<p>그저 내부용으로 사용하려고 했던, 1시간만에 만든 간단한 코드였습니다. 초기버전에서는 상태에 따른 예외처리 옵션도 설정할 수 있었지만 공개한 라이브러리에는 상태 코드에 따른 예외 처리 기능은 제거했습니다. 사용하시는 분들이 interceptor로 충분히 직접 구현하실 수 있기 때문입니다.</p>
<p>스크린샷의  <code>returnCustomizedFetch</code>는 간단한 고계함수(함수를 인수로 받거나 결과 값으로 반환하는 함수)입니다. 매개변수로 받은 <code>defaultOptions</code>가 적용된 <code>fetch</code>함수를 return합니다. <code>fetch</code>를 호출하기 전에 request interceptor에서 전처리를 하고 호출해서 받은 response를 response interceptor에서 후처리 합니다. <strong>return하는 함수는 <code>fetch</code> 함수와 완전히 같은 인터페이스를 갖습니다.</strong></p>
<p>만들어놓고 보니 조금만 손보면 범용적으로 사용할 수 있어보였습니다. 초기 버전에서는 전역에 선언되어 있는 <code>fetch</code>를 직접 사용했지만, <code>fetch</code>를 <code>defaultOptions</code> 으로 받을 수 있다면 모든 환경에서 사용할 수 있는 코드를 만들 수 있을 것 같았습니다.</p>
<h4 id="재귀적인-타입-선언-무한으로-즐기는-interceptor">재귀적인 타입 선언, 무한으로 즐기는 interceptor</h4>
<ol>
<li><code>defaultOptions</code>로 받은 fetch에 baseUrl, default headers, interceptor를 적용한 <code>fetch</code>함수를 return합니다.</li>
<li>이 <code>fetch</code>함수를 다시 <code>defaultOptions</code>에 넣어서 새로운 기능을 추가합니다.</li>
<li>2번에서 만든 함수를 다시 <code>defaultOptions</code>에 넣어서 새로운 기능을 추가합니다.</li>
<li>3번에서 만든 함수를 다시...</li>
</ol>
<p>전역에 선언된 <code>fetch</code>를 사용하는게 아니라 매개변수로 <code>fetch</code>를 받아서 사용한다면 코드는 외부 세계를 참조하지 않는 순수한 함수가 됩니다. 여기서 &#39;순수한&#39;이라는 의미는 함수형 프로그래밍의 관점에서 순수하다는 의미인데, 함수가 스코프 바깥 세계를 참조하지 않기 때문에 같은 매개변수를 넣으면 언제나 동일한 <code>fetch</code>함수를 return한다는 뜻입니다.</p>
<p><code>fetch</code>를 return하는 함수가 매개변수로 <code>fetch</code>를 받는 재귀적인 타입 구조는 사용자가 원하는 만큼 필요할 때마다 <code>fetch</code>를 쉽게 확장할 수 있게 해줍니다. 뭔가 아름다운 구조를 만든 것 같아서 저는 신나서 PR을 올렸습니다.</p>
<p><img src="https://cdn.myeongjae.kim/blog/2023/08/fetch-3.png" alt=""></p>
<p>스크린샷의 <code>fetchOnClient</code>는 3가지 기능을 합성한 fetch함수입니다.</p>
<pre><code class="language-ts">export const fetchOnClient = returnFetchForDeerBackend({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoader({
      headers: defaultHeaders,
      baseUrl: ENV.NEXT_PUBLIC_BASE_URL,
    }),
  }),
});</code></pre>
<ol>
<li>비동기 요청을 보내기 전에 로딩화면을 보여주고 요청이 끝나면 로딩화면을 제거합니다.</li>
<li>400이상의 상태를 받았을 때 예외를 던집니다.</li>
<li>response body에서 특정한 필드를 추출합니다.</li>
</ol>
<p>만들어놓고 보니 <code>returnFetch</code>함수는 지금까지 배우고 경험했던 코드에 비추어 봤을 때 부끄럽지 않은 코드였고, 아직 한국에서 Next.js의 App Router가 대중화된 상황은 아니니 라이브러리로 만들어 공개한다면 한국의 Next.js 생태계에 기여할 수 있을 것 같았습니다. 그래서 공개합니다: <a href="https://return-fetch.myeongjae.kim/">return-fetch</a>
<img src="https://return-fetch.myeongjae.kim/meta.png" alt="return-fetch, A simple and powerful high order function to extend fetch"></p>
<h2 id="return-fetch-한-번-맛보세요"><code>return-fetch</code> 한 번 맛보세요!</h2>
<p><img src="https://cdn.myeongjae.kim/blog/2023/08/fetch-4.png" alt=""></p>
<p>위에서 말씀드렸듯이 <code>return-fetch</code>는 Next.js의 클라이언트 사이드와 서버 사이드에서 모두 사용할 수 있는 <code>fetch</code> 확장 라이브러리를 찾다 찾다 못 찾아서 직접 구현한 라이브러리입니다. 딱 필요한 3가지 기능만을 구현하면서 다른 라이브러리들이 가진 단점을 모두 제거했습니다.</p>
<p>실제 코드는 여기서 볼 수 있습니다: <a href="https://github.com/deer-develop/return-fetch/blob/main/src/index.ts#L148">https://github.com/deer-develop/return-fetch/blob/main/src/index.ts#L148</a>. 짧고 간단한 코드지만 이 코드 덕분에 저는 Axios 없이도 빠르게 제품 개발을 할 수 있었습니다. 이 코드를 작성하면서 중요하게 생각했던 것들과 어떻게 응용할 수 있는지 자세하게 말씀드리겠습니다.</p>
<h3 id="return-fetch의-핵심-철학"><code>return-fetch</code>의 핵심 철학</h3>
<ol>
<li><strong>딱 3가지 기능만</strong> 구현합니다<ul>
<li>baseUrl, default headers, interceptors</li>
</ul>
</li>
<li>아무런 라이브러리를 의존하지 않고 <strong>오직 타입스크립트</strong>만 사용합니다.</li>
<li><strong>LSP(Liskov substitution principle)</strong> 를 지킵니다.<ul>
<li>기존에 <code>fetch</code>를 사용하고 있던 모든 곳을 <code>return-fetch</code>로 교체하더라도 문제없이 작동합니다.</li>
</ul>
</li>
<li>사용자가 interceptor를 쉽게 추가할 수 있어야 합니다.</li>
<li>사용자가 <strong>SRP(Single responsibility principle)</strong> 를 지킬 수 있어야 합니다.<ul>
<li>사용자가 직접 한 번에 하나의 기능을 제공하는 interceptor를 작성하고, interceptor가 적용된 <code>fetch</code>를 조합하면서 원하는 기능이 적용된 <code>fetch</code>를 생성할 수 있습니다.</li>
</ul>
</li>
</ol>
<h3 id="어떻게-쓰나요">어떻게 쓰나요?</h3>
<p><code>return-fetch</code>는 <code>fetch</code>함수를 return하는 함수입니다. 매개변수로 baseUrl, default header와 interceptor를 받아서 기본값이 적용된 fetch함수를 만듭니다.</p>
<pre><code class="language-ts">import returnFetch from &quot;return-fetch&quot;;

const fetchExtended = returnFetch({
  baseUrl: &quot;https://jsonplaceholder.typicode.com&quot;,
  headers: { Accept: &quot;application/json&quot; },
  interceptors: {
    request: async (args) =&gt; {
      console.log(&quot;********* before sending request *********&quot;);
      console.log(&quot;url:&quot;, args[0].toString());
      console.log(&quot;requestInit:&quot;, args[1], &quot;\n\n&quot;);
      return args;
    },

    response: async (response, requestArgs) =&gt; {
      console.log(&quot;********* after receiving response *********&quot;);
      console.log(&quot;url:&quot;, requestArgs[0].toString());
      console.log(&quot;requestInit:&quot;, requestArgs[1], &quot;\n\n&quot;);
      return response;
    },
  },
});

fetchExtended(&quot;/todos/1&quot;, { method: &quot;GET&quot; })
  .then((it) =&gt; it.text())
  .then(console.log);</code></pre>
<p><code>returnFetch</code>의 return값인 <code>fetchExtended</code>를 <code>fetch</code>대신 사용하면 됩니다. <code>fetchExtended</code>는 <code>fetch</code>와 완전히 동일한 인터페이스(매개변수와 리턴타입)를 갖기 때문에 기존에 사용하던 <code>fetch</code>처럼 그대로 사용하면 됩니다.</p>
<p>위 예제는 API요청을 보내기 전에 request interceptor에서 로그를 출력하고, API요청의 응답을 받은 후에 repsonse interceptor에서 로그를 출력합니다. request interceptor의 매개변수는 baseUrl과 default header가 적용된 상태입니다.</p>
<p>request interceptor의 return값을 API요청할 때 사용하기 때문에 요청을 보내기 전에 request interceptor에서 요청에 대한 전처리를 할 수 있고, response interceptor의 return값이 <code>fetchExtended</code>함수의 return값이 되기 때문에 response interceptor에서 응답에 대한 후처리를 할 수 있습니다.</p>
<p><a href="https://stackblitz.com/edit/return-fetch">https://stackblitz.com/edit/return-fetch</a> 여기에서 실제로 라이브러리를 사용해보실 수 있습니다.</p>
<h3 id="return-fetch의-장단점"><code>return-fetch</code>의 장단점</h3>
<h4 id="장점-3">장점</h4>
<ul>
<li>가볍습니다 (<strong>733Byte</strong>, minified + gzipped)</li>
<li><code>fetch</code>가 있는 <strong>어떤 실행환경에서도 사용할 수 있습니다</strong> (Node.js, 웹브라우저, React Native, Web Worker 등등).</li>
<li>모든 <code>fetch</code> polyfill과 호환됩니다.</li>
<li>Side effect가 없습니다. 전역 fetch를 건드리지 않습니다. 외부 세계의 상태를 바꾸지 않으므로 안심하고 사용할 수 있습니다.</li>
<li>재귀적인 타입 선언 덕분에 특별한 처리 없이도 interceptor를 원하는 만큼 추가할 수 있습니다.</li>
<li>테스트 커버리지 100%를 유지합니다. 모든 기능을 테스트했습니다.</li>
<li>Next.js App Router환경에서 잘 작동합니다. Next.js를 사용하시는 분들이라면 마음놓고 쓰셔도 됩니다.</li>
</ul>
<h4 id="단점-3">단점</h4>
<ul>
<li>뼈대만 제공합니다. 필요한 기능이 있다면 직접 구현해야 합니다.<ul>
<li>baseUrl과 default header 기능도 request interceptor로 직접 구현할 수 있지만, 이 두 가지 기능은 정말 많이 쓰이기 때문에 편의상 구현해놨습니다.</li>
</ul>
</li>
<li>신규 라이브러리라서 쓰는 사람이 별로 없습니다(😢).</li>
</ul>
<h3 id="필요한-기능이-있나요-제가-만들어놨습니다">필요한 기능이 있나요? 제가 만들어놨습니다.</h3>
<p>이렇게 작고 간단한 라이브러리로 뭘 할 수 있을까요? 제가 원하는 기능은 모두 다 만들 수 있었습니다.</p>
<p>예를 들어서</p>
<ol>
<li>요청 보내기 전에 로딩 화면을 띄우고 요청 보낸 후에 로딩 화면을 제거하기</li>
<li>응답으로 400이상의 상태를 받았을 때 에러를 던지기</li>
<li>request body를 string이 아니라 json으로 받고, response body도 json으로 return하는 fetch함수 만들기</li>
<li>401응답을 받았을 때 쿠키를 제거하고 요청을 다시 한 번 보내기</li>
<li><code>returnFetch</code> 를 사용하는 함수를 조합해서 기능 합치기</li>
<li>전역 fetch 교체하기</li>
</ol>
<p>이 정도가 있습니다. 필요한 게 있으시면 <a href="https://github.com/deer-develop/return-fetch/issues">https://github.com/deer-develop/return-fetch/issues</a> 이슈에 올려주세요! 제가 한 번 만들어 보겠습니다 😎</p>
<h4 id="1-요청-보내기-전에-로딩-화면을-띄우고-요청-보낸-후에-로딩-화면을-제거하기">1. 요청 보내기 전에 로딩 화면을 띄우고 요청 보낸 후에 로딩 화면을 제거하기</h4>
<p>간단한 예제입니다. <code>returnFetch</code>와 동일한 타입의 함수인 <code>returnFetchWithLoadingIndicator</code>가 return하는 함수를 fetch대신 사용하면 됩니다.</p>
<pre><code class="language-ts">import returnFetch, { ReturnFetch } from &quot;return-fetch&quot;;
import { displayLoadingIndicator, hideLoadingIndicator } from &quot;@/your/adorable/loading/indicator&quot;;

// Write your own high order function to display/hide loading indicator
const returnFetchWithLoadingIndicator: ReturnFetch = (args) =&gt; returnFetch({
  ...args,
  interceptors: {
    request: async (args) =&gt; {
      setLoading(true);
      return args;
    },
    response: async (response) =&gt; {
      setLoading(false);
      return response;
    },
  },
})

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchWithLoadingIndicator({
  // default options
});</code></pre>
<p><a href="https://return-fetch.myeongjae.kim/#1-displayhide-loading-indicator">https://return-fetch.myeongjae.kim/#1-displayhide-loading-indicator</a> 여기에서 위 코드를 직접 실행해보실 수 있습니다.</p>
<p>다만 실제로 저 코드를 사용하게 되면 API를 호출할 때마다 로딩화면이 깜빡거려서 거슬리는 느낌을 받습니다. 저희 제품에는 응답 지연시간이 200ms 이상일 때만 로딩화면이 보이도록 조치해서 사용하고 있습니다(이 기능도 역시 <code>returnFetch</code>로 구현했습니다).</p>
<h4 id="2-응답으로-400이상의-상태를-받았을-때-에러를-던지기">2. 응답으로 400이상의 상태를 받았을 때 에러를 던지기</h4>
<pre><code class="language-ts">import returnFetch, { ReturnFetch } from &quot;return-fetch&quot;;

// Write your own high order function to throw an error if a response status is more than or equal to 400.
const returnFetchThrowingErrorByStatusCode: ReturnFetch = (args) =&gt; returnFetch({
  ...args,
  interceptors: {
    response: async (response) =&gt; {
      if (response.status &gt;= 400) {
        throw await response.text().then(Error);
      }

      return response;
    },
  },
})

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchThrowingErrorByStatusCode({
  // default options
});</code></pre>
<p><a href="https://return-fetch.myeongjae.kim/#2-throw-an-error-if-a-response-status-is-more-than-or-equal-to-400">https://return-fetch.myeongjae.kim/#2-throw-an-error-if-a-response-status-is-more-than-or-equal-to-400</a> 여기에서 위 코드를 직접 실행해보실 수 있습니다.</p>
<h4 id="3-request-body를-string이-아니라-json으로-받고-response-body도-json으로-return하는-fetch함수-만들기">3. request body를 string이 아니라 json으로 받고, response body도 json으로 return하는 fetch함수 만들기</h4>
<p>Axios처럼 response body의 타입을 generic으로 지정할 수 있습니다. 아래 코드는 별도의 패키지 공개했습니다: <a href="https://npmjs.com/package/return-fetch-json">return-fetch-json</a></p>
<pre><code class="language-ts">import returnFetch, { FetchArgs, ReturnFetchDefaultOptions } from &quot;return-fetch&quot;;

// Use as a replacer of `RequestInit`
type JsonRequestInit = Omit&lt;NonNullable&lt;FetchArgs[1]&gt;, &quot;body&quot;&gt; &amp; { body?: object };

// Use as a replacer of `Response`
export type ResponseGenericBody&lt;T&gt; = Omit&lt;
  Awaited&lt;ReturnType&lt;typeof fetch&gt;&gt;,
  keyof Body | &quot;clone&quot;
&gt; &amp; {
  body: T;
};

export type JsonResponse&lt;T&gt; = T extends object
  ? ResponseGenericBody&lt;T&gt;
  : ResponseGenericBody&lt;unknown&gt;;


// this resembles the default behavior of axios json parser
// https://github.com/axios/axios/blob/21a5ad34c4a5956d81d338059ac0dd34a19ed094/lib/defaults/index.js#L25
const parseJsonSafely = (text: string): object | string =&gt; {
  try {
    return JSON.parse(text);
  } catch (e) {
    if ((e as Error).name !== &quot;SyntaxError&quot;) {
      throw e;
    }

    return text.trim();
  }
};

// Write your own high order function to serialize request body and deserialize response body.
export const returnFetchJson = (args?: ReturnFetchDefaultOptions) =&gt; {
  const fetch = returnFetch(args);

  return async &lt;T&gt;(
    url: FetchArgs[0],
    init?: JsonRequestInit,
  ): Promise&lt;JsonResponse&lt;T&gt;&gt; =&gt; {
    const response = await fetch(url, {
      ...init,
      body: init?.body &amp;&amp; JSON.stringify(init.body),
    });

    const body = parseJsonSafely(await response.text()) as T;

    return {
      headers: response.headers,
      ok: response.ok,
      redirected: response.redirected,
      status: response.status,
      statusText: response.statusText,
      type: response.type,
      url: response.url,
      body,
    } as JsonResponse&lt;T&gt;;
  };
};

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchJson({
  // default options
});

//////////////////// Use it somewhere ////////////////////
export type ApiResponse&lt;T&gt; = {
  status: number;
  statusText: string;
  data: T;
};

fetchExtended&lt;ApiResponse&lt;{ message: string }&gt;&gt;(&quot;/sample/api/echo&quot;, {
  method: &quot;POST&quot;,
  body: { message: &quot;Hello, world!&quot; }, // body should be an object.
}).then(it =&gt; it.body);</code></pre>
<p>약간 길지만 기본적인 구현은 이전 예제들과 동일합니다. <a href="https://return-fetch.myeongjae.kim/#3-serialize-request-body-and-deserialize-response-body">https://return-fetch.myeongjae.kim/#3-serialize-request-body-and-deserialize-response-body</a> 여기에서 실행해볼 수 있습니다.</p>
<h4 id="4-401응답을-받았을-때-쿠키를-제거하고-요청을-다시-한-번-보내기">4. 401응답을 받았을 때 쿠키를 제거하고 요청을 다시 한 번 보내기</h4>
<pre><code class="language-ts">let retryCount = 0;

const returnFetchRetry: ReturnFetch = (args) =&gt; returnFetch({
  ...args,
  interceptors: {
    response: async (response, requestArgs, fetch) =&gt; {
      if (response.status !== 401) {
        return response;
      }

      console.log(&quot;not authorized, trying to get refresh cookie..&quot;);
      const responseToRefreshCookie = await fetch(
        &quot;https://httpstat.us/200&quot;,
      );
      if (responseToRefreshCookie.status !== 200) {
        throw Error(&quot;failed to refresh cookie&quot;);
      }

      retryCount += 1;
      console.log(`(#${retryCount}) succeeded to refresh cookie and retry request`);
      return fetch(...requestArgs);
    },
  },
});

const fetchExtended = returnFetchRetry({
  baseUrl: &quot;https://httpstat.us&quot;,
});

fetchExtended(&quot;/401&quot;)
  .then((it) =&gt; it.text())
  .then((it) =&gt; `Response body: &quot;${it}&quot;`)
  .then(console.log)
  .then(() =&gt; console.log(&quot;\n Total counts of request: &quot; + (retryCount + 1)))</code></pre>
<p>response interceptor에서 응답을 확인하고 상태가 401이라면 retry를 합니다. <a href="https://return-fetch.myeongjae.kim/#8-retry-a-request">https://return-fetch.myeongjae.kim/#8-retry-a-request</a> 여기에서 실행해보실 수 있습니다.</p>
<h4 id="5-returnfetch-를-사용하는-함수를-조합해서-기능-합치기">5. <code>returnFetch</code> 를 사용하는 함수를 조합해서 기능 합치기</h4>
<p>제가 <code>returnFetch</code>에서 가장 좋아하는 기능입니다. 이전 예제들은 각각 하나의 기능만 구현했는데, 만약 로딩 화면도 보여주고싶고 상태가 400이상일 때 에러도 던지고싶고 request, response body를 객체로 사용하고 싶으면 어떻게 해야 할까요?</p>
<p>코드를 다시 작성할 필요 없이 위 함수들을 재활용해서 조합하면 됩니다.</p>
<pre><code class="language-ts">import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from &quot;@/your/customized/return-fetch&quot;;

/*
  Compose high order functions to create your awesome fetch.
   1. Add loading indicator.
   2. Throw an error when a response&#39;s status code is 400 or more.
   3. Serialize request body and deserialize response body as json and return it.
*/
export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      // default options
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended(&quot;/sample/api/echo&quot;, {
  method: &quot;POST&quot;,
  body: { message: &quot;Hello, world!&quot; }, // body should be an object.
}).catch((e) =&gt; { alert(e.message); });</code></pre>
<p><a href="https://return-fetch.myeongjae.kim/#4-compose-above-three-high-order-functions-to-create-your-awesome-fetch-">https://return-fetch.myeongjae.kim/#4-compose-above-three-high-order-functions-to-create-your-awesome-fetch-</a> 여기에서 실행해보실 수 있습니다.</p>
<p><code>returnFetch</code>는 매개변수로 <code>fetch</code>함수를 받을 수 있습니다. <code>returnFetch</code>의 return값은 <code>fetch</code>함수의 인터페이스와 동일하기 때문에 <code>fetch</code>대신 사용할 수 있으므로 <code>returnFetch</code>의 매개변수에 자리에도 들어갈 수 있습니다. <code>returnFetch</code>는 기능이 추가된 <code>fetch</code>를 계속 감싸면서 기능을 덧붙일 수 있습니다.</p>
<p>특별한 처리 없이 재귀적인 타입 선언만으로 복수의 interceptor를 처리할 수 있는 것이 <code>returnFetch</code>의 최대 장점입니다. 위에서 보시는 것처럼 하나의 기능만 추가하는 함수를 작성해서 하나의 책임만 지도록 할 수 있기 때문에 <code>returnFetch</code>의 사용자는 단일 책임 원칙(Single Responsibility Principle)을 지킬 수 있습니다. 라이브러리에서 별도의 interceptor 중첩 처리를 하지 않기 때문에 번들 사이즈도 작게 유지할 수 있었습니다.</p>
<h4 id="6-전역-fetch-교체하기">6. 전역 fetch 교체하기</h4>
<p><code>returnFetch</code>는 리스코프 치환 원칙(Liskov substitution principle)을 만족하기 때문에 <code>returnFetch</code>가 생성한 함수를 전역 <code>fetch</code>대신 사용할 수 있습니다. 전역 <code>fetch</code>를 사용하는 모든 곳에서 로딩 화면을 보여주고 400이상의 응답을 받았을 때 예외를 던지고 싶다면 아래처럼 함수를 조합하고 <code>fetch</code>를 덮어씌우면 됩니다.</p>
<pre><code class="language-ts">import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from &quot;@/your/customized/return-fetch&quot;;

// save global fetch reference.
const globalFetch = globalThis.fetch;
export const fetchExtended = returnFetchThrowingErrorByStatusCode({
  fetch: returnFetchWithLoadingIndicator({
    fetch: globalFetch, // use global fetch as a base.
  }),
});

// replace global fetch with your customized fetch.
globalThis.fetch = fetchExtended;</code></pre>
<p>전역 공간을 건드리는 행위는 절대 추천하지 않습니다. 하지만 꼭 필요한 순간도 있을테니, 그 때도 <code>returnFetch</code>를 문제없이 사용할 수 있습니다.</p>
<h3 id="nextjs에서-return-fetch를-어떻게-사용하면-좋을까요">Next.js에서 <code>return-fetch</code>를 어떻게 사용하면 좋을까요?</h3>
<p>위에서 말씀드린 것처럼 Next.js에서 API요청을 하는 3가지 맥락이 있습니다.</p>
<ol>
<li>클라이언트 사이드(웹브라우저)에서 AJAX 요청</li>
<li>서버 사이드에서 렌더링하기 위한 AJAX 요청</li>
<li>서버 사이드에서 렌더링이 아니라 프록시 역할로 AJAX 요청을 하고 응답을 그대로 내보냄(<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route">route.ts 파일</a>)</li>
</ol>
<h4 id="1-클라이언트-사이드웹브라우저에서-ajax-요청">1. 클라이언트 사이드(웹브라우저)에서 AJAX 요청</h4>
<p>클라이언트 사이드에서 AJAX 요청을 할 때는 3가지 기능을 추가해서 사용하고 있습니다.</p>
<ol>
<li>요청을 보내기 전에 로딩화면을 보여주고 응답을 받으면 로딩화면을 제거한다.</li>
<li>응답의 상태 코드가 400이상이라면 에러를 던진다</li>
<li>request, response body를 객체로 넣으면 자동으로 직렬화/역직렬화를 한다. repsonse body의 타입은 generic으로 지정할 수 있다.</li>
</ol>
<h4 id="2-서버-사이드에서-렌더링하기-위한-ajax-요청">2. 서버 사이드에서 렌더링하기 위한 AJAX 요청</h4>
<p>서버 사이드 렌더링용 데이터를 가져올 때는 2가지 기능을 추가해서 사용하고 있습니다.</p>
<ol>
<li>응답의 상태 코드가 400이상이라면 에러를 던진다</li>
<li>request, response body를 객체로 넣으면 자동으로 직렬화/역직렬화를 한다. repsonse body의 타입은 generic으로 지정할 수 있다.</li>
</ol>
<h4 id="3-서버-사이드에서-렌더링이-아니라-프록시-역할로-ajax-요청을-하고-응답을-그대로-내보냄routets-파일">3. 서버 사이드에서 렌더링이 아니라 프록시 역할로 AJAX 요청을 하고 응답을 그대로 내보냄(route.ts 파일)</h4>
<p>서버 사이드에서 프록시 역할을 해야 할 때(<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route">route.ts 파일</a>)는 기능을 추가하지 않고 baseUrl과 defaultHeader만 지정해서 사용하고 있습니다. 받은 response를 별도의 처리 없이 그대로 client에 전달하기 때문입니다.</p>
<h2 id="마무리">마무리</h2>
<p>fetch를 다양한 환경에서 사용해야하는 Next.js와 <code>return-fetch</code>는 잘 어울립니다. Axios에 익숙하지만 Next.js의 App Router를 사용할 예정이라면 Axios와 fetch사이에서 갈등할 수밖에 없습니다. 갈등의 순간에 제 라이브러리가 현명한 선택을 도울 수 있으면 좋겠습니다.</p>
<p>request, response body를 객체로 받을 수 있게 해주는 코드는 많이 쓰일 것 같아서 쉽게 받아서 사용하실 수 있도록 <a href="https://npmjs.com/package/return-fetch-json">npm에 새로운 패키지로 등록</a>했습니다. <code>return-fetch</code> 사랑해주세요! (신규 라이브러리라 사용하기가 부담스럽다면 <a href="">ofetch</a>를 추천합니다, 번들 사이즈가 <strong>7배</strong> 정도 크지만...)</p>
<p>감사합니다.</p>
<h2 id="부록--fetch-library-비교">부록:  <code>fetch</code> library 비교</h2>
<h3 id="1-fetch-intercept-1">1. fetch-intercept</h3>
<p><a href="https://www.npmjs.com/package/fetch-intercept">https://www.npmjs.com/package/fetch-intercept</a></p>
<h4 id="장점-4">장점</h4>
<ul>
<li>가벼운 번들 사이즈 (762 Byte, minified + gzipped)</li>
</ul>
<h4 id="단점-4">단점</h4>
<ul>
<li>baseUrl을 설정할 수 없다.</li>
<li>default headers를 설정할 수 없다.</li>
<li>전역에 선언된  <code>fetch</code> 에 몽키패치를 해버리기 때문에 모든 <code>fetch</code> 호출에 request, response interceptor가 적용된다. <code>fetch</code>를 다양한 맥락에서 사용해야 하는 Next.js에는 어울리지 않는다.</li>
</ul>
<h3 id="2-wretch-1">2. wretch</h3>
<p><a href="https://www.npmjs.com/package/wretch">https://www.npmjs.com/package/wretch</a></p>
<h4 id="장점-5">장점</h4>
<ul>
<li>다양한 기능 (baseUrl, default header, interceptor 모두 지원)</li>
<li>상대적으로 가벼운 번들 사이즈 (1.9KB, minified + gzipped)</li>
</ul>
<h4 id="단점-5">단점</h4>
<ul>
<li>Axios처럼 response status에 따라 에러를 던지기 때문에 Next.js의 route.ts에서 사용하려면 추가 조치가 필요하다.</li>
<li>wretch 인스턴스를 생성할 때 뿐만 아니라 사용할 때도 fetch와 호환되지 않는 자신만의 인터페이스를 가지고 있기 때문에 추가로 학습이 필요하다.</li>
</ul>
<h3 id="3-ofetch-1">3. ofetch</h3>
<p><a href="https://www.npmjs.com/package/ofetch">https://www.npmjs.com/package/ofetch</a></p>
<h4 id="장점-6">장점</h4>
<ul>
<li>다양한 기능 (baseUrl, default header, interceptor 모두 지원)</li>
<li>Next.js의 route.ts에서도 사용할 수 있도록 response 상태와 상관 없이 예외를 던지지 않고 응답을 전달할 수 있다.</li>
<li>fetch와 호환되는 인터페이스를 가졌다.</li>
</ul>
<h4 id="단점-6">단점</h4>
<ul>
<li>Next.js에서 ofetch를 사용하는 레퍼런스가 부족하다. App Router도 새로운 기술인데 여기에 ofetch까지 얹어서 사용하기는 부담스럽다.</li>
<li>필요한 기능에 비해 번들 사이즈가 크다 (5KB)</li>
</ul>
<h3 id="4-return-fetch">4. return-fetch</h3>
<p><a href="https://www.npmjs.com/package/return-fetch">https://www.npmjs.com/package/return-fetch</a></p>
<h4 id="장점-7">장점</h4>
<ul>
<li>가볍다 (<strong>733Byte</strong>, minified + gzipped)</li>
<li><code>fetch</code>만 있다면 <strong>어떤 실행환경에서도 사용할 수 있다</strong> (Node.js, 웹브라우저, React Native, Web Worker 등등).</li>
<li>모든 <code>fetch</code> polyfill과 호환된다.</li>
<li>Side effect가 없다. 전역 fetch를 건드리지 않는다. 외부 세계의 상태를 바꾸지 않으므로 안심하고 사용할 수 있다.</li>
<li>재귀적인 타입 선언 덕분에 특별한 처리 없이도 interceptor를 원하는 만큼 추가할 수 있다.</li>
<li>테스트 커버리지가 100%다. 모든 기능을 테스트했다.</li>
<li>Next.js App Router환경에서 잘 작동한다. Next.js 사용자는 마음놓고 써도 된다.</li>
</ul>
<h4 id="단점-7">단점</h4>
<ul>
<li>뼈대만 제공한다. 필요한 기능이 있다면 직접 구현해야 한다.<ul>
<li>baseUrl, default header 기능도 request interceptor만 있으면 직접 구현할 수 있지만, 다행히 이 두 가지 기능은 탑재되어 있다.</li>
</ul>
</li>
<li>신규 라이브러리라서 쓰는 사람이 별로 없다(😢).</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>