Quick Start

TypeScript용 RxJS 기반 타입 세이프 HTTP 클라이언트 + (선택) Next.js/RSocket 어댑터.


핵심 특징

  • Swagger/OpenAPI / AsyncAPI 파생 스키마 / 커스텀 계약 등으로부터 얻은 OpenAPI-style Paths 타입(관례상 paths)을 주입해 라우트(요청) 타입 안정성을 확보
  • 모든 API는 RxJS Observable 반환
  • core는 프레임워크 독립 (Next.js 의존 없음)
  • Next.js 전용 기능은 @byeolnaerim/typed-rx-http/next 엔트리포인트로 분리 → /next를 import할 때만 Next.js가 필요
  • RSocket 전용 기능은 @byeolnaerim/typed-rx-http/rsocket 엔트리포인트로 분리 → /rsocket을 import할 때만 RSocket 패키지가 필요

설치

npm
bash
npm i @byeolnaerim/typed-rx-http rxjs

엔트리포인트

Core (프레임워크 독립)
ts
import {
  createHttpClient,
  createHeaderStore,
  createCsrCache,
  createSessionAuth,
  HttpResponseError,
  isHttpResponseError,
  type ServiceArguments,
  type CacheForService,
} from "@byeolnaerim/typed-rx-http";
Next.js 어댑터(선택)
ts
import {
  callApiSsrCache,
  redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";

Next.js를 사용하지 않는 프로젝트에서는 /next를 import하지 마세요.

RSocket 어댑터(선택)

RSocket을 쓰는 프로젝트에서만 peer 패키지를 설치하세요.

bash
npm i rsocket-core rsocket-types rsocket-websocket-client
ts
import { createRSocketApi } from "@byeolnaerim/typed-rx-http/rsocket";

const api = createRSocketApi("ws://localhost:7000/rsocket", jwt);

api.mono<User>("user.findById", { id: 1 }).subscribe((user) => {
  console.log(user);
});

api.stream<Notification>("notification.stream").subscribe((notification) => {
  console.log(notification);
});

RSocket을 사용하지 않는 프로젝트에서는 /rsocket을 import하지 마세요.

Core 사용법

1) Paths 타입 준비 (보통은 OpenAPI paths)

createHttpClient<Paths>()의 Paths는 “요청 스펙(라우트)”을 표현하는 타입입니다. 문서에서는 관례상 paths라고 부르지만, 꼭 OpenAPI/Swagger일 필요도, 이름이 paths일 필요도 없습니다.

다만 코어는 내부적으로 OpenApiPathsLike 제약을 사용하므로, Paths는 아래처럼 OpenAPI paths와 유사한 형태여야 합니다.

  • 최상위 키: URL 경로 문자열(예: "/users/{id}")
  • 하위 키: HTTP method (get/post/put/delete/patch …)
  • 각 method 안에 parameters.query/path/header/cookie, requestBody, responses 같은 필드가 존재(또는 never)

코어는 위 구조에서 주로 아래 필드를 참조해 ServiceArguments의 타입을 구성합니다.

  • url: keyof Paths
  • method: keyof Paths[url]
  • queryString: parameters.query
  • pathVariable: parameters.path
  • body: requestBody

예: openapi-typescript 출력물은 보통 아래와 같은 규격입니다(일부 축약).

ts
export interface paths {
  "/test/get-hello-world": {
    parameters: {
      query?: never;
      header?: never;
      path?: never;
      cookie?: never;
    };
    get: {
      parameters: {
        query?: never;
        header?: never;
        path?: never;
        cookie?: never;
      };
      requestBody?: never;
      responses: {
        200: {
          content: {
            "application/json": components["schemas"]["TestResponse"];
          };
        };
      };
    };
  };
}
ts
// aliasing: OpenAPI `paths` -> `Paths`
import type { paths as Paths } from "./@types/ApiTypes";

2) HeaderStore 생성

HeaderStore는 CSR에서 기본 헤더를 관리하기 위한 간단한 in-memory store입니다.

ts
import { createHeaderStore } from "@byeolnaerim/typed-rx-http";

export const headerStore = createHeaderStore({
  "Content-Type": "application/json",
});

3) HTTP client 생성

  • headerStore는 선택이지만, CSR에서 기본 헤더/세션 인증을 사용하려면 넣는 것을 권장합니다.
  • headersProvider는 SSR/멀티테넌트처럼 요청마다 헤더 계산이 필요할 때 사용합니다.
ts
import { createHttpClient } from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

const apiUrl = process.env.NEXT_PUBLIC_API_URL!;

export const client = createHttpClient<Paths>({
  baseUrl: apiUrl,
  headerStore,
});

4) API 호출 (타입 세이프)

요청 타입(url/method/pathVariable/queryString/body)은 사용자가 createHttpClient<Paths>()에 주입한 타입(관례상 OpenAPI paths)으로부터 결정됩니다. 응답 타입은 callApi<R>()에서 호출자가 제너릭 R로 선택합니다(코어가 responses에서 자동 추론하지 않습니다).

ts
export const getAdminList = () => {
  return client.callApi<{ items: any[]; total: number }>({
    url: "/api/account/search/admin/get-list",
    method: "get",
    queryString: { pageNumber: 1, pageSize: 20 },
  });
};

응답 래핑(ResponseWrapper) — 선택

이 라이브러리는 응답 래핑을 강제하지 않습니다. API별로 제너릭으로 응답 형태를 선택하면 됩니다.

래핑된 응답
ts
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };

export const bidCreate = (body: unknown) => {
  return client.callApi<ResponseWrapper<{ requestId: string }>>({
    url: "/api/bid/create/bid/request",
    method: "post",
    body,
  });
};
래핑 없는 응답
ts
export const bidCreate = (body: unknown) => {
  return client.callApi<{ requestId: string }>({
    url: "/api/bid/create/bid/request",
    method: "post",
    body,
  });
};

스트리밍 (NDJSON)

서버가 NDJSON(한 줄에 JSON 하나)을 내려줄 때 callApiStream을 사용합니다. Accept 헤더가 없으면 기본값으로 application/x-ndjson가 설정됩니다.

ts
export const streamEvents = () => {
  return client.callApiStream<{ eventName: string; content: unknown }>({
    url: "/api/event/stream",
    method: "get",
    // headers: { Accept: "application/x-ndjson" }, // 선택
  });
};

CSR 캐시 (클라이언트 캐시)

createCsrCache<CacheName>()가 제공하는 기능:

  • callApiCsrCache(callApiFn, serviceArgs, cacheOptions)
  • removeCsrCache(cacheName) — 타입 캐시명 + 문자열 모두 지원
ts
import { createCsrCache } from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

type CacheNames = "adminList" | "profile";

const csrCache = createCsrCache<CacheNames>();

export const callApiClientCache = <R>(
  args: ServiceArguments<Paths, any, any, R>,
  cache: { cacheName: CacheNames; cacheTime: number; cacheSize?: number },
) => csrCache.callApiCsrCache(client.callApi, args as any, cache as any);

// 무효화
csrCache.removeCsrCache("adminList"); // 타입
csrCache.removeCsrCache("any-prefix"); // 문자열

세션 기반 인증 플러그인 (선택)

createSessionAuth는 세션 인증 로직을 코어에서 분리해 옵션으로 붙였다 떼는 방식입니다.

동작:

  • Authorization을 headerStore에 유지
  • ensureToken$()로 토큰 동기화(/api/auth/token)
  • 401 발생 시 refresh 1회 시도(/api/auth/token/refresh) 후 원 요청 재시도
  • refresh 실패 시 logout(/api/auth/logout) 후 에러 전달
  • 로그인 상태 변경은 onLoginChange 콜백으로 외부에서 처리
ts
import { createSessionAuth } from "@byeolnaerim/typed-rx-http";

const auth = createSessionAuth({
  headerStore,
  onLoginChange: (loggedIn) => {
    // 예: setLogin(loggedIn) 같은 외부 상태 업데이트(선택)
  },
  // tokenUrl / refreshUrl / logoutUrl 커스터마이징 가능
});

export const secureCall = () => {
  return client
    .callApi<{ ok: true }>({ url: "/api/secure", method: "get" })
    .pipe(auth.withSessionAuth());
};

refresh/retry 없이 토큰 동기화만 필요하면:

ts
return client.callApi(...).pipe(auth.withEnsureToken());

에러 처리

2xx가 아니면 HttpResponseError를 throw 합니다(status, response, args, data 포함).

레거시 호환: 에러 바디가 { resultType: ... } 형태면 그 객체를 그대로 throw 합니다.

ts
import { isHttpResponseError } from "@byeolnaerim/typed-rx-http";

client.callApi(...).subscribe({
  error: (e) => {
    if (isHttpResponseError(e)) {
      console.log(e.status, e.data);
    }
  },
});

Next.js 어댑터 (/next)

redirectToUnauthorizedOnServer401

redirectToUnauthorizedOnServer401는 Next.js(App Router) SSR 환경에서 401이 발생했을 때 redirect를 수행하는 기본 구현(편의 함수)입니다.

ts
import { redirectToUnauthorizedOnServer401 } from "@byeolnaerim/typed-rx-http/next";

동작 규칙(고정):

  • redirect 대상: /unauthorized
  • queryString: redirect_uri=<현재 페이지> + logout=true
  • 현재 페이지는 x-page-url 헤더에서 읽습니다(없으면 /)

즉, 위 경로/쿼리 규칙이 프로젝트와 맞을 때만 그대로 사용하세요. 경로가 다르거나 쿼리 규칙이 다르면, 아래처럼 직접 onServer401를 구현해서 주입하면 됩니다.

ts
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export const onServer401 = async () => {
  const h = await headers();
  const pageUrl = h.get("x-page-url") || "/";
  redirect(`/login?next=${encodeURIComponent(pageUrl)}`);
};
callApiSsrCache

Next의 next/cache(unstable_cache) 기반 SSR 캐시 도우미입니다.

  • GET + cacheTime > 0 → force-cache + revalidate
  • 그 외 → no-store
  • headersProvider로 요청별 Cookie / Authorization 주입
  • 401 발생 시 onServer401가 있으면 실행(보통 redirect())
ts
import type { paths as Paths } from "./@types/ApiTypes";
import { firstValueFrom } from "rxjs";
import {
  callApiSsrCache,
  redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";

const apiUrl = process.env.NEXT_PUBLIC_API_URL!;

const headersProvider = async () => {
  const { cookies } = await import("next/headers");
  const cookieStore = await cookies();

  const cookieHeader = cookieStore
    .getAll()
    .map((c) => `${c.name}=${c.value}`)
    .join("; ");

  const accessToken = cookieStore.get("accessToken")?.value;

  return {
    ...(cookieHeader ? { Cookie: cookieHeader } : {}),
    ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
  };
};

const obs$ = callApiSsrCache<Paths, { ok: true }, "adminList">({
  baseUrl: apiUrl,
  serviceArguments: { url: "/api/secure", method: "get" },
  cacheForService: { cacheName: "adminList", cacheTime: 10_000 },
  headersProvider,
  onServer401: redirectToUnauthorizedOnServer401,
});

const res = await firstValueFrom(obs$);

Next.js 통합 예시: 프로젝트 rxjsHttpService/commonService 전체 코드

아래 rxjsHttpService.ts가 이후 generator 예제의 commonServiceFile이 가리키는 프로젝트 공용 HTTP adapter입니다. 라이브러리가 생성하는 파일이 아니라 프로젝트가 직접 관리하며, core client + session auth + CSR cache + SSR cache helper를 한 곳에서 export합니다. 아래는 전체 코드입니다.

rxjsHttpService.ts
ts
import {
	createCsrCache,
	createHeaderStore,
	createHttpClient,
	createSessionAuth,
} from "@byeolnaerim/typed-rx-http";

import type {
	CacheForService,
	ServiceArguments,
} from "@byeolnaerim/typed-rx-http";

import { defer, Observable } from "rxjs";

import { setLogin } from "@ui/handler/hooks/useAccounts";

import type { paths } from "./@types/ApiTypes";

import type { CacheNames } from "./@types/CacheNames";

export const redirectToUnauthorizedOnServer401 = async () => {
	const { headers } = await import("next/headers");
	const { redirect } = await import("next/navigation");

	const h = await headers();
	const pageUrl = h.get("x-page-url") || "/";
	const redirectUri = encodeURIComponent(pageUrl);

	redirect(`/unauthorized?redirect_uri=${redirectUri}&logout=true`);
};

export const headerStore = createHeaderStore({
	"Content-Type": "application/json",
});

const headersProvider = async () => {
	if (typeof window !== "undefined") {
		/**
		 * 라이브러리쪽 코드가
		 * if (headersProvider) return headersProvider();
		 * if (headerStore) return headerStore.get();
		 * 이렇게 되어있음 같이 못씀, 방향자체는 맞음. 라이브러리쪽에서 marge해버리면 csr/ssr 구분못함
		 */
		return headerStore.get();
	}

	const { cookies } = await import("next/headers");

	const cookieStore = await cookies();
	const cookieHeader = cookieStore
		.getAll()
		.map((c) => `${c.name}=${c.value}`)
		.join("; ");

	const authorization = cookieStore.get("Authorization")?.value;
	const accessToken = cookieStore.get("accessToken")?.value;

	return {
		...(cookieHeader ? { Cookie: cookieHeader } : {}),
		...(authorization
			? {
					Authorization: authorization.startsWith("Bearer ")
						? authorization
						: `Bearer ${authorization}`,
				}
			: {}),
		...(!authorization && accessToken
			? {
					Authorization: accessToken.startsWith("Bearer ")
						? accessToken
						: `Bearer ${accessToken}`,
				}
			: {}),
	};
};

export const service = createHttpClient<paths>({
	baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
	onServer401: redirectToUnauthorizedOnServer401,
	defaultErrorMessage: () => {
		return "알 수 없는 오류가 발생했습니다.";
	},
	headerStore,
	headersProvider,
	dropAuthWhenCacheControl: true,
});

const sessionAuth = createSessionAuth({
	headerStore,
	tokenUrl: "/api/auth/token",
	refreshUrl: "/api/auth/token/refresh",
	logoutUrl: "/api/auth/logout",
	onLoginChange: (loggedIn) => {
		if (typeof window !== "undefined") {
			setLogin(loggedIn);
		}
	},
});

export const callApi = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApi<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

export const callApiStream = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApiStream<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

// CSR cache
const csrCache = createCsrCache<CacheNames>();

export const callApiClientCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	return csrCache.callApiCsrCache(callApi, serviceArguments, cacheForService);
};

export const removeCsrCache = csrCache.removeCsrCache;

export const callApiServerCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	if (typeof window !== "undefined") {
		return callApiClientCache(serviceArguments, cacheForService);
	}

	return new Observable<R>((subscriber) => {
		let closed = false;

		import("@byeolnaerim/typed-rx-http/next")
			.then(({ callApiSsrCache }) => {
				if (closed) {
					return;
				}

				const subscription = callApiSsrCache<paths, R, CacheNames>({
					baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
					serviceArguments: serviceArguments as ServiceArguments<
						paths,
						keyof paths,
						keyof paths[keyof paths] & string,
						R
					>,
					cacheForService,
					headersProvider,
					onServer401: redirectToUnauthorizedOnServer401,
				}).subscribe(subscriber);

				subscriber.add(() => {
					closed = true;
					subscription.unsubscribe();
				});
			})
			.catch((error) => {
				if (!closed) {
					subscriber.error(error);
				}
			});

		return () => {
			closed = true;
		};
	});
};

API 레퍼런스 (core)

createHttpClient<Paths>(options)

반환:

  • callApi<R>(args): Observable<R>
  • callApiStream<RChunk>(args): Observable<RChunk>
  • uploadFile({ file, url, ifNoneMatch?, headers? }): Observable<Response>
  • createSSEObservable<R>(args): Observable<R>

옵션:

  • baseUrl: string
  • headerStore?: HeaderStore
  • headersProvider?: () => Record<string, string> | Promise<Record<string, string>>
  • dropAuthWhenCacheControl?: boolean (기본값: true)
  • onServer401?: () => void | Promise<void>
createHeaderStore(initial?)

get(), set(), merge(), remove(), clear()

createCsrCache<CacheName>()
  • callApiCsrCache(callApiFn, serviceArgs, cacheForService)
  • removeCsrCache(cacheName) (타입 + 문자열)
createSessionAuth(options)
  • withSessionAuth(), withEnsureToken()
  • ensureToken$(), refreshToken$(), logout$()

런타임 요구사항

  • fetch / Response API 사용(rxjs/fetch)
  • 스트리밍(NDJSON)은 ReadableStream + TextDecoder 필요
  • SSE는 EventSource 필요

대부분의 최신 브라우저와 Next.js 런타임에서는 기본 제공됩니다. 커스텀 Node 런타임에서는 폴리필이 필요할 수 있습니다.

Auto Node Script: OpenAPI/Swagger 코드 생성(선택)

이 패키지는 런타임 HTTP 클라이언트와 별도로, OpenAPI/Swagger JSON에서 타입/서비스 코드를 생성하는 Node 스크립트를 함께 제공합니다. 이 스크립트는 선택 기능입니다.

일반적인 @byeolnaerim/typed-rx-http, /next, /rsocket 사용자는 이 스크립트를 실행하지 않아도 되며, openapi-typescript를 설치할 필요도 없습니다.

ts
import {
  connectToSwaggerEventStream,
  generateSwaggerFromHttp,
  generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";
의존성 격리

OpenAPI 타입 생성에는 openapi-typescript CLI가 필요합니다. 하지만 이 패키지는 openapi-typescript를 일반 dependencies에 넣지 않습니다.

@byeolnaerim/typed-rx-http 라이브러리 자체는 devDependencies.typescript로 TypeScript 6.0.3을 사용합니다. typed-rx-http, /next, /rsocket 엔트리포인트와 라이브러리 빌드는 이 TypeScript 6.0.3 기준을 유지합니다.

다만 openapi-typescript는 아직 특정 TypeScript 5.x 버전을 요구할 수 있으므로, OpenAPI auto node script만 별도의 npx 임시 실행 환경에서 openapi-typescript와 [email protected]을 같이 실행합니다. 이 임시 실행 환경은 라이브러리의 devDependencies.typescript 6.0.3을 바꾸지 않고, 사용자의 프로젝트에 설치된 typescript나 openapi-typescript 버전도 사용하지 않습니다.

기본값은 auto script 실행 시점에만 아래 명령을 생성해서 실행하는 것입니다.

bash
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts

따라서 사용법 자체는 바뀌지 않습니다. 기존처럼 auto node script를 호출하면 되고, OpenAPI 타입 생성 단계에서만 격리된 TypeScript 5.9.3 환경이 사용됩니다. auto script를 쓰지 않는 사용자는 openapi-typescript나 TypeScript 5.9.3에 전혀 묶이지 않습니다.

필요하면 openApiTypescriptCommand로 명령을 직접 고정할 수 있습니다.

ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
  openApiTypescriptCommand:
    "npx -y -p [email protected] -p [email protected] openapi-typescript ./swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts",
});

또는 기본 명령을 구성하는 패키지 버전만 바꿀 수도 있습니다.

ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
  openApiTypescriptPackage: "[email protected]",
  openApiTypescriptTypescriptPackage: "[email protected]",
});
생성되는 파일

기본 설정은 아래 파일들을 생성합니다.

text
swagger.json
src/handler/service/@types/ApiTypes.d.ts
src/handler/service/auto/*Service.ts
src/handler/service/@types/auto/*Types.ts
src/handler/service/apiUnionArrays.ts

apiUnionArrays.ts는 OpenAPI schema enum뿐 아니라 query, path, header, cookie parameter enum도 상수 배열로 생성합니다. 배열 query parameter의 items.enum도 처리합니다.

EventStream 감시
ts
connectToSwaggerEventStream({
  hostname: "localhost",
  port: 8788,
  path: "/oauth2/for-local/get-swagger",
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  commonServiceFile: "./src/handler/service/rxjsHttpService.ts",
});
HTTP 1회 요청
ts
await generateSwaggerFromHttp({
  hostname: "localhost",
  port: 8788,
  path: "/oauth2/for-local/get-swagger",
});
로컬 파일에서 생성
ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
});

기존 프로젝트 통합 예제: WebFlux + Swagger 자동 생성

여기부터는 백엔드 Swagger와 자동 생성 service를 함께 사용하는 프로젝트 통합 예제입니다. 위의 Core 사용법과 선택 기능은 typed-rx-http 자체만으로 사용할 수 있으며, 아래 흐름은 Swagger 기반 service 자동 생성을 사용하는 프로젝트에서 추가로 적용합니다.

1. 백엔드에 REST endpoint 작성

백엔드 코드는 평소처럼 작성합니다. 이 예제에서는 path variable로 이름을, query parameter로 메시지를 받아 Mono로 응답합니다.

TypedRxHttpExampleRouter.java
java
@Bean
public RouterFunction<ServerResponse> typedRxHttpExample(
  TypedRxHttpExampleHandler handler
) {
  return route()
    .GET("/typed-rx-http/example/{name}", handler::getExample)
    .build();
}

2. Swagger에서 프론트 서비스 생성

백엔드의 swagger.json을 받아 타입과 service 파일을 생성합니다. 이 작업은 개발 서버를 실행할 때 한 번 호출하거나 watch script로 연결할 수 있습니다.

generateSwagger.cjs
js
const {
  generateSwaggerFromHttp,
} = require("@byeolnaerim/typed-rx-http/auto-node-script/openapi");

generateSwaggerFromHttp({
  hostname: "localhost",
  port: 8123,
  path: "/for-local/get-swagger",
  swaggerFile: "./swagger.json",
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  typesFile: "./src/handler/service/@types/ApiTypes.d.ts",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

3. 생성된 함수로 요청 보내기

아래에서 Front code와 Backend code를 바꿔 볼 수 있습니다. Result를 누르면 오른쪽에 실행 화면이 열립니다. 값을 바꾼 뒤 요청 버튼을 눌러보세요.

TypedRxHttpRequestExample.tsx
tsx
import { byName } from "@handler/service/auto/TypedRxHttpExampleService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  byName({
    path: {
      name: name.trim(),
    },
    params: {
      message: message.trim() || undefined,
    },
  }),
);

setResponse(response);
© 2026 Byeolnaerim. All rights reserved.소개개인정보처리방침