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);
}
}
ํ์์์ ํ์ด๋จธ๋ฅผ ์ ๋ฆฌํ์ฌ ๋ฉ๋ชจ๋ฆฌ/ํ์ด๋ฐ ๋์๋ฅผ ๋ฐฉ์งํ๋ค.
'Trouble Shooting & Issues > Linkiving' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| 401 Unauthorization ์๋ฌ์ ์์ธ ์ฐพ๊ธฐ (1) | 2026.03.12 |
|---|---|
| Docker ์ค์นํด์ ๋ฐฑ์๋ ๋ก์ปฌ๋ก ๋ก๊ทธ์ธ ํ ์คํธํ๊ธฐ [Linkiving] (0) | 2026.02.27 |
| safeFetch ์ ํธํจ์ ๋ง๋ค๊ธฐ(1) : Error ํด๋์ค ์ ์ (0) | 2025.12.12 |
| Next.js app router๋ก api ํจ์ ์์ฑํ๊ธฐ (0) | 2025.11.27 |
| [Next.js] ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋งํฌ์ ์ธ๋ค์ผ ๊ฐ์ ธ์ค๊ธฐ (0) | 2025.11.17 |
GitHub