safeFetch ์œ ํ‹ธํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ(2): safeFetch ํ•จ์ˆ˜ ์ž‘์„ฑ

2025. 12. 12. 01:57ใ†Trouble Shooting & Issues/Linkiving

๋ฐ˜์‘ํ˜•

 

โœ”๏ธ SafeFetchOptions๋ฅผ ์ •์˜ํ•˜์—ฌ RequestInit(method, headers, ...) ์™ธ ์ถ”๊ฐ€์ ์ธ ์˜ต์…˜์„ ํ™œ์šฉํ•˜์—ฌ fetchํ•œ๋‹ค.
โœ”๏ธ safeFetch์—์„œ๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ ์ฒดํฌ, ๋ณธ๋ฌธ ๋ฐ ์—๋Ÿฌ ๊ธธ์ด ๊ฒ€์‚ฌ, content type ๊ฒ€์‚ฌ, ํŒŒ์‹ฑ ์—๋Ÿฌ ๊ฒ€์‚ฌ ๋“ฑ์„ ์ง„ํ–‰ํ•˜์—ฌ ์•ˆ์ „ํ•œ fetch๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ, ์•ˆ์ •์ ์ธ ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.

๋ชฉ์ฐจ

1. SafeFetchOptions : safeFetch๊ฐ€ ๋ฐ›์„ ์ถ”๊ฐ€ ์˜ต์…˜

2. safeFetch() : ๊ฐ์ข… ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์—ฌ ์•ˆ์ „ํ•œ fetch๋ฅผ ํ•˜๋Š” ํ•จ์ˆ˜

 

 

safeFetch ์œ ํ‹ธ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ(1) : Error ํด๋ž˜์Šค ์ •์˜

 

safeFetch ์œ ํ‹ธ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ(1) : Error ํด๋ž˜์Šค

โœ”๏ธ FetchError : ์‘๋‹ต ๋ฐ›๋Š” ์ค‘ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ํด๋ž˜์Šค ์ƒ์„ฑโœ”๏ธ TimeoutError : ์ง€์ •๋œ ์‹œ๊ฐ„ ๋‚ด ์‘๋‹ต์„ ๋ฐ›์ง€ ๋ชปํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ํด๋ž˜์Šค ์ƒ์„ฑโœ”๏ธ ParseError : ์‘๋‹ต์€ ๋ฐ›์•˜์œผ๋‚˜, ํŒŒ์‹ฑ ์ค‘ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ํด

ldd6cr-adness.tistory.com

 


1. SafeFetchOptions : safeFetch๊ฐ€ ๋ฐ›์„ ์ถ”๊ฐ€ ์˜ต์…˜

safeFetch์— ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“  ์˜ต์…˜ ํƒ€์ž…์ด๋‹ค.

๊ธฐ๋ณธ fetch()์˜ RequestInit ์˜ต์…˜์€ ๋‹จ์ˆœ ์š”์ฒญ ์„ค์ •๋งŒ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ๋Š”

์ถ”๊ฐ€์ ์ธ ์•ˆ์ •์„ฑ,๊ฒ€์ฆ,๋ณด์•ˆ,๋””๋ฒ„๊น… ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ™•์žฅ๋œ ์˜ต์…˜์„ ์ž‘์„ฑํ–ˆ๋‹ค.

 

โœ… RequestInit์ด๋ž€?
๋ธŒ๋ผ์šฐ์ € Fetch API์˜ ํ‘œ์ค€ ํƒ€์ž…์œผ๋กœ, fetch()์˜ ๋‘๋ฒˆ์งธ ์ธ์ž์— ๋“ค์–ด๊ฐ€๋Š” ๋ชจ๋“  ์˜ต์…˜ ์ง‘ํ•ฉ์ด๋‹ค.
method, headers, body, signel, credentials, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.

 

 

export interface SafeFetchOptions extends RequestInit {
  timeout?: number;
  expectJson?: boolean;
  maxResponseBytes?: number;
  maxErrorBodyBytes?: number;
}
  • timeout : ์š”์ฒญ์ด ์ง€์ • ์‹œ๊ฐ„ timeout ๋‚ด์— ๋๋‚˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ TimeoutError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์กฐ๊ฑด
  • expectJson : true์ผ ๊ฒฝ์šฐ, Content-Type: application/json ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค.
    ๋Œ€๋ถ€๋ถ„์˜ API๋Š” JSON์„ ๋‚ด๋ ค์ฃผ์ง€๋งŒ, ์˜ˆ์™ธ์ ์œผ๋กœ ํ…์ŠคํŠธ๋‚˜ ํŒŒ์ผ์„ ์ฃผ๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์ „๊ฒ€์ฆ ์š”์†Œ๋ฅผ ๋„ฃ๋Š”๋‹ค.
  • maxResponseBytes : ์‘๋‹ต ๋ณธ๋ฌธ ๊ธธ์ด๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ์ง€์ •ํ•œ ๋ฐ”์ดํŠธ ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•  ๊ฒฝ์šฐ, ์•ˆ์ „์ƒ ์ด์œ ๋กœ ์š”์ฒญ์„ ์‹คํŒจ ์ฒ˜๋ฆฌํ•œ๋‹ค.
    ์„œ๋ฒ„ ์˜ค๋ฅ˜๋‚˜ ์•…์˜์  ์‘๋‹ต์œผ๋กœ ๋งค์šฐ ํฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๊ฒฝ์šฐ, ๋ฉ”๋ชจ๋ฆฌ ๊ธ‰์ฆ, ๋ธŒ๋ผ์šฐ์ € ํƒญ ๋‹ค์šด, ์„ฑ๋Šฅ ์ด์Šˆ ๋“ฑ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ์‚ฌ์ „ ๋ฐฉ์–ดํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.
  • maxErrorBodyBytes : ํŒŒ์‹ฑ ์‹คํŒจ์‹œ ParseError.raw์— ํฌํ•จํ•  ๋ณธ๋ฌธ ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ œํ•œํ•œ๋‹ค.
    parse ์—๋Ÿฌ๋กœ ์ธํ•ด ๋ฐ›์€ raw body๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ์œผ๋ฉด ๋กœ๊ทธ๊ฐ€ ๋„ˆ๋ฌด ์ปค์ ธ ์ €์žฅ ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•˜๊ณ  ์ฝ˜์†”์ด ๋‚œ์žกํ•ด์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 


2. safeFetch() : ๊ฐ์ข… ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์—ฌ ์•ˆ์ „ํ•œ fetch๋ฅผ ํ•˜๋Š” ํ•จ์ˆ˜

๋“œ๋””์–ด safeFetch๋ฅผ ์ž‘์„ฑํ•  ์ฐจ๋ก€๊ฐ€ ์™”๋‹ค.

 

2-1. ํ•จ์ˆ˜ ์„ ์–ธ๋ถ€

export async function safeFetch<T = unknown>(url: string, options: SafeFetchOptions = {}): Promise<T>
  { ... }

์šฐ์„  ํ•จ์ˆ˜ ์„ ์–ธ๋ถ€๋Š” ์ด์™€ ๊ฐ™์ด ์ž‘์„ฑ๋œ๋‹ค.

url, options๋ฅผ props๋กœ ๋ฐ›์•„ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•˜๊ณ , ์„ฑ๊ณต ์‹œ ์ œ๋„ค๋ฆญ T ํƒ€์ž…์˜ ๊ฐ’์„ ๋น„๋™๊ธฐ๋กœ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์—๋Ÿฌ๋ฅผ ๋˜์ง€๋Š” ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

์ด ๋•Œ options๋Š” ์œ„์—์„œ ์ •์˜ํ–ˆ๋˜ SafeFetchOptions ํƒ€์ž…์ด๋ฉฐ, ๊ธฐ๋ณธ๊ฐ’์€ ๋นˆ ๊ฐ์ฒด์ด๋‹ค.

 

โœ… T(์ œ๋„ค๋ฆญ)์ด๋ž€?
T๋Š” ํ˜ธ์ถœ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์‘๋‹ต ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ํƒ€์ž… ๋งค๊ฐœ๋ณ€์ˆ˜์ด๋‹ค.
๋งŒ์•ฝ safeFetch<User>๋ผ๊ณ  ํ˜ธ์ถœํ•˜๋ฉด TS๋Š” T ์ž๋ฆฌ์— User๋ฅผ ๋„ฃ์–ด safeFetch๊ฐ€ Promise<User>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ ํƒ€์ž… ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

type User = { id: string; name: string };
const user = await safeFetch<User>('/api/me');

๋‹จ, T๋Š” ์ปดํŒŒ์ผ ์‹œ์˜ ๋„์›€์œผ๋กœ, ๋Ÿฐํƒ€์ž„์—์„œ ์‹ค์ œ๋กœ ์„œ๋ฒ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ User ํ˜•์‹์„ ๋ณด๋ƒˆ๋Š”์ง€๋Š” ๊ฒ€์ฆํ•˜์ง€ ์•Š๋Š”๋‹ค.
๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ์„ ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด zod์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

 

 

 

2-2. options ์ดˆ๊ธฐํ™”

const {
    timeout = 10_000,
    expectJson = true,
    maxResponseBytes,
    maxErrorBodyBytes = 1024 * 8, // ์—๋Ÿฌ์‹œ์— ๋กœ๊น…ํ•  ์ตœ๋Œ€ ๋ฐ”์ดํŠธ
    ...fetchOptions
  } = options;

ํ•จ์ˆ˜๊ฐ€ ์‹œ์ž‘ํ•˜๋ฉด options๋ฅผ ์ดˆ๊ธฐํ™”๋ถ€ํ„ฐ ํ•œ๋‹ค.

  • timeout : ์š”์ฒญ ํƒ€์ž„์•„์›ƒ(ms)์œผ๋กœ, ๊ธฐ๋ณธ๊ฐ’์€ 10์ดˆ์ด๋‹ค.
  • expectJson : ๊ธฐ๋ณธ์ ์œผ๋กœ JSON ์‘๋‹ต์„ ๊ธฐ๋Œ€ํ•œ๋‹ค.
  • maxResponseBytes : ์กด์žฌ ์‹œ, ์‘๋‹ต ํฌ๊ธฐ ์ œํ•œ ๊ฒ€์‚ฌ์— ์‚ฌ์šฉํ•œ๋‹ค. (๊ธฐ๋ณธ๊ฐ’์€ ์—†์Œ)
  • maxErrorBodyBytes : ์—๋Ÿฌ ๋กœ๊น… ์‹œ ํฌํ•จํ•  ์ตœ๋Œ€ ๋ฐ”์ดํŠธ์ด๋‹ค. (๊ธฐ๋ณธ๊ฐ’ 8KB)
  • ...fetchOptions : RequestInit์—์„œ ๋ฐ›์€ ๋‚˜๋จธ์ง€ ์˜ต์…˜(method, headers,body, credentials emd)๋“ค์ด๋‹ค.

์ด ๋•Œ fetchOptions์— signal์ด ๋“ค์–ด์žˆ๋‹ค๋ฉด ์ด์ „์— ์ž‘์„ฑํ•œ controller.signal๋กœ ๋ฎ์–ด์“ฐ๊ฒŒ ๋˜๋ฏ€๋กœ ์ด๋ถ€๋ถ„์— ๋Œ€ํ•œ ๊ทœ์น™์„ ์ •ํ•ด์•ผํ•œ๋‹ค.

 

โœ… ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ ๋นˆ ๊ฐ์ฒด๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฃผ๊ณ  ์‹ค์ œ ๋””ํดํŠธ ๊ฐ’ ์ง€์ •์„ ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ํ•œ ์ด์œ 
SafeFetchOptions์—๋งŒ ํ•ด๋‹นํ•˜๋Š” ์˜ต์…˜๊ณผ RequestInit ์˜ต์…˜์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
์‹ค์ œ๋กœ ์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด timeout, expectJson, maxResponseBytes, maxErrorBodyBytes์™€ fetchOptions๋กœ ๊ตฌ์กฐ๋ถ„ํ•ด๋ฅผ ํ–ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž์ฒด ํ™•์žฅ ์˜ต์…˜๊ณผ Fetch์— ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•˜๋Š” fetchOptions๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค.

 

 

2-3. AbortController ์ƒ์„ฑ ๋ฐ ํƒ€์ž„์•„์›ƒ ์„ค์ •

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
  • controller : new AbortController()๋กœ ์š”์ฒญ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋Š” ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ ๋‹ค.
  • id : setTimeout()์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ํƒ€์ด๋จธ ํ•ธ๋“ค์„ ์ €์žฅํ•˜๋Š” ๋ณ€์ˆ˜์ด๋‹ค.
    setTimeout()์€ ์ˆซ์ž, ํ˜น์€ Timeout ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ํƒ€์ด๋จธ๋ฅผ ์ทจ์†Œํ•˜๊ธฐ ์œ„ํ•ด ์ด ๊ฐ’์„ ๋ณด๊ด€ํ•ด๋‘๋Š” ๊ฒƒ์ด๋‹ค.
    ์ง€์ •ํ•œ timeout ์ดํ›„์— setTimeout์œผ๋กœ controller.abort()๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๊ฐ•์ œ์ข…๋ฃŒ๋ฅผ ํ•œ๋‹ค.
    ์—ฌ๊ธฐ์„œ ์„ ์–ธํ•œ id๋Š” ์ดํ›„ finally์—์„œ clearTimeout(id)๋กœ ๋ฐ˜๋“œ์‹œ ํ•ด์ œํ•ด์•ผ ๋ฉ”๋ชจ๋ฆฌ/ํƒ€์ด๋ฐ ์ด์Šˆ๋ฅผ ๋ง‰๋Š”๋‹ค.

 

2-4. try๋ฌธ ์‹คํ–‰

์ด์ œ๋ถ€ํ„ฐ ๋‚˜์˜ฌ try๋ฌธ์ด ์ข€ ๊ธด๋ฐ ๋‚ด์šฉ์„ ๋ณด๋ฉด ๋Œ€๊ฐ• ์•„๋ž˜์™€ ๊ฐ™์ด ์ง„ํ–‰๋œ๋‹ค.

1. fetch ํ˜ธ์ถœ → res = await fetch( ... )

2. HTTP ์ƒํƒœ ์ฝ”๋“œ ์ฒดํฌ → if (!res.ok) { ... }

3. Content๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ Length ๊ฒ€์‚ฌ → if (typeof maxResponseBytes === 'number') { ... }

4. JSON ๊ธฐ๋Œ€ ์‹œ Content-Type ๊ฒ€์‚ฌ → if (expectJson) { ... }

5. JSON ํŒŒ์‹ฑ → try { ... }

 

1. fetch ํ˜ธ์ถœ

try {
    const res = await fetch(url, { ...fetchOptions, signal: controller.signal });
  • fetchOptions๋ฅผ ์ „๋‹ฌํ•˜๋ฉฐ signal๋กœ AbortController๋ฅผ ์—ฐ๊ฒฐํ•œ๋‹ค.
  • await์„ ๊ฑธ์–ด์„œ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ, ์ด ๋‹จ๊ณ„์—์„œ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด catch์—์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.

2. HTTP ์ƒํƒœ ์ฝ”๋“œ ์ฒดํฌ

if (!res.ok) {
      // ์—๋Ÿฌ ๋ฐ”๋”” ์ฝ๊ธฐ (์ž‘๊ฒŒ ์ œํ•œ)
      let bodyText: string | undefined;
      try {
        const text = await res.text();
        bodyText = text.length > maxErrorBodyBytes ? text.slice(0, maxErrorBodyBytes) + '…' : text;
      } catch {
        bodyText = undefined;
      }
      throw new FetchError(`Request failed with status ${res.status}`, {
        status: res.status,
        body: bodyText,
        contentType: res.headers.get('content-type'),
      });
    }

fetch๋กœ ์‘๋‹ต(res)์„ ๋ฐ›์•„์™”๋Š”๋ฐ res.ok๊ฐ€ false์ด๋ฉด ์„œ๋ฒ„๊ฐ€ ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๊ฒƒ์œผ๋กœ, FetchError๋ฅผ ๋˜์ง„๋‹ค.

  • res.text() : ์—๋Ÿฌ ๋ฐ”๋””๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ฝ์–ด ๋””๋ฒ„๊น…์šฉ์œผ๋กœ ์ผ๋ถ€๋ฅผ ์ €์žฅํ•œ๋‹ค.
    ์ด ๋•Œ body๊ฐ€ ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ, maxErrorBodyBytes๋งŒํผ ์ž๋ฅธ๋‹ค.
  • throw new FetchError : ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ status, body, contentType์„ ํฌํ•จํ•œ ์ปค์Šคํ…€ ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.

 

3. Content๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ Length ๊ฒ€์‚ฌ

if (typeof maxResponseBytes === 'number') {
      const cl = res.headers.get('content-length');
      if (cl && !isNaN(Number(cl)) && Number(cl) > maxResponseBytes) {
        throw new FetchError(`Response too large: ${cl} bytes`, {
          contentType: res.headers.get('content-type'),
        });
      }
    }

maxResponseBytes๋ฅผ ์ง€์ •ํ–ˆ์„ ๊ฒฝ์šฐ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ์ด๋‹ค.

Content-Length ํ—ค๋”๋ฅผ ์ฝ์—ˆ์„ ๋•Œ ์‘๋‹ต์ด ๋„ˆ๋ฌด ํฌ๋ฉด ์‹คํŒจ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค.

  • cl : ์‘๋‹ต ์ค‘ 'content-length' ํ—ค๋”์˜ ๊ฐ’์„ cl์— ์ €์žฅํ•œ๋‹ค.
  • cl์ด ๋„ˆ๋ฌด ํฌ๊ฑฐ๋‚˜ maxResponseBytes๋ณด๋‹ค ํฌ๋‹ค๋ฉด message, content-type์„ ์ง€์ •ํ•˜์—ฌ FetchError๋ฅผ ๋˜์ง„๋‹ค.

๋‹จ, Content-Length ํ—ค๋”๋Š” ํ•ญ์ƒ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค. ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์ด ๊ฒ€์‚ฌ๋Š” ๋ฌด์‹œ๋œ๋‹ค.

 

โœ… ์–ด๋–ค ๊ฒฝ์šฐ์— Content-Length ํ—ค๋”๊ฐ€ ์—†๋Š” ๊ฑธ๊นŒ
1. ์ฒญํฌ ์ „์†ก
: HTML/1.1์—์„œ ๋ณธ๋ฌธ ๊ธธ์ด๋ฅผ ๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ๊ฐ(chunk) ๋‹จ์œ„๋กœ ๋ณด๋‚ธ ํ›„ ๋งˆ์ง€๋ง‰์— ์ข…๋ฃŒ ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ธ๋‹ค.
2. ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต (Server-Sent Events, Web-ready JSON stream ๋“ฑ)
3. gzip ๋“ฑ์˜ ์••์ถ•๋œ ์‘๋‹ต์„ ํ˜๋ ค ๋ณด๋‚ด๋Š” ๊ฒฝ์šฐ : ์••์ถ• ์ „ ์›๋ณธ ํฌ๊ธฐ๋ฅผ ์„œ๋ฒ„๊ฐ€ ๋ชจ๋ฅผ ์ˆ˜ ์žˆ์Œ
4. ํ”„๋ก์‹œ/๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๊ฐ€ content-length๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๊ฒฝ์šฐ

 

 

 

4. JSON ๊ธฐ๋Œ€ ์‹œ Content-Type ๊ฒ€์‚ฌ

const contentType = res.headers.get('content-type');
    if (expectJson) {
      if (!contentType || !contentType.includes('application/json')) {
        // ์ผ๋ถ€ API๋Š” ์• ๋งคํ•˜๊ฒŒ application/json; charset=utf-8 ์œผ๋กœ ์˜ค๋ฏ€๋กœ includes ์‚ฌ์šฉ
        const snippet = await res.clone().text().catch(() => undefined);
        throw new FetchError('Invalid content-type, expected JSON', {
          contentType,
          body: snippet ? snippet.slice(0, maxErrorBodyBytes) : undefined,
        });
      }
    }

expectJson์ด true์ผ ๊ฒฝ์šฐ, Content-Type์— application/json์ด ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์—†์œผ๋ฉด FetchError๋ฅผ ๋˜์ง„๋‹ค.

  • ์ผ๋ถ€ API๋Š” application/json; charset=utf-8๊ณผ ๊ฐ™์ด ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ถ™์–ด์˜ฌ ์ˆ˜๋„ ์žˆ๊ธฐ์— includes๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • ํƒ€์ž…์ด ๋ถˆ์ผ์น˜ํ•˜๋ฉด ๋””๋ฒ„๊น…์šฉ์œผ๋กœ ์ผ๋ถ€ body(snippet)๋ฅผ ์ฝ์–ด์„œ FetchError์— body๋กœ ๋„ฃ๋Š”๋‹ค.
  • ์ด ๋•Œ res.text(), res.json()์€ body๋ฅผ ์†Œ๋น„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง„๋‹จ์šฉ์œผ๋กœ body๋ฅผ ์ฝ์„ ๋•Œ๋Š” clone()์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

5. JSON ํŒŒ์‹ฑ

try {
      const json = (await res.json()) as T;    // ์ œ๋„ค๋ฆญ T๋กœ ๋ฐ˜ํ™˜
      return json;
    } catch (err) {
      let raw: string | undefined;  // ํŒŒ์‹ฑ ์‹คํŒจ์‹œ ์›๋ณธ ์ผ๋ถ€๋ฅผ ์ถ”์ถœํ•ด์„œ ํฌํ•จ
      try {
        raw = await res.text();
      } catch {
        raw = undefined;
      }
      throw new ParseError('Failed to parse JSON response', raw?.slice(0, maxErrorBodyBytes));
    }
  }
  • const json : await์„ ๊ฑธ๊ณ  JSON์„ ํŒŒ์‹ฑํ•œ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด ์ œ๋„ค๋ฆญ T๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ catch๋กœ ์žก์•„์„œ ParseError์— raw๋ฅผ ๋‹ด์•„ ๋˜์ง„๋‹ค.

 

2-5. catch (err)

catch (err) {
    if (err instanceof Error && (err.name === 'AbortError' || (err as any).name === 'TimeoutError')) {
      throw new TimeoutError(`Request to ${url} aborted after ${timeout}ms`);
    }
    if (err instanceof FetchError || err instanceof ParseError || err instanceof TimeoutError) {
      throw err;
    }
    throw new FetchError(String(err instanceof Error ? err.message : err), { contentType: null });
  }

์ด ์„ธ ์ข…๋ฅ˜์˜ ์—๋Ÿฌ๋ฅผ ๋˜์ง€๊ณ  ์žˆ๋‹ค.

  • AbortError, TimeoutError
    AbortController.abort()๋กœ ์ธํ•œ ์˜ค๋ฅ˜๋Š” ๋ธŒ๋ผ์šฐ์ €/Node์—์„œ AbortError๋กœ ๊ฐ์ง€ํ•œ๋‹ค.
    Abort ์ƒํ™ฉ์€ ํƒ€์ž„์•„์›ƒ์œผ๋กœ ํ•ด์„ํ•˜๊ธฐ์— ๊ฐ™์ด TimeoutError๋กœ ๋˜์ง„๋‹ค.
  • FetchError, ParseError, TimeoutError
    ์ปค์Šคํ…€ ์—๋Ÿฌ์ผ ๊ฒฝ์šฐ ๊ทธ๋Œ€๋กœ ๋‹ค์‹œ ๋˜์ง„๋‹ค.
    ์ปค์Šคํ…€ ์—๋Ÿฌ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ํฌํ•จ์‹œํ‚ค๋„๋ก ์ด๋ฏธ ๋งŒ์ง„ ์—๋Ÿฌ์ธ๋ฐ FetchError๋กœ ๋˜ ๊ฐ์‹ธ๋ฉด ์œ ์‹ค๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • Error
    ๊ทธ ์™ธ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๋Š” FetchError๋กœ ๋ž˜ํ•‘ํ•ด์„œ ๋˜์ง„๋‹ค.

 

 

2-6. finally

finally {
    clearTimeout(id);
  }
}

ํƒ€์ž„์•„์›ƒ ํƒ€์ด๋จธ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ/ํƒ€์ด๋ฐ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.

 

 

 

 

 

๋ฐ˜์‘ํ˜•