diff --git a/frontend/src/components/SearchLayout.empty-live-region.test.tsx b/frontend/src/components/SearchLayout.empty-live-region.test.tsx
new file mode 100644
index 000000000..0578e46be
--- /dev/null
+++ b/frontend/src/components/SearchLayout.empty-live-region.test.tsx
@@ -0,0 +1,131 @@
+/* @vitest-environment jsdom */
+import React, { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("next/dynamic", () => ({
+ default: () => function MockDynamic() {
+ return
mock graph
;
+ },
+}));
+
+vi.mock("next/link", () => ({
+ default: ({ href, children, ...props }: React.AnchorHTMLAttributes & { href: string }) => (
+ {children}
+ ),
+}));
+
+vi.mock("lucide-react", () => ({
+ AlertCircle: () => ,
+ CalendarDays: () => ,
+ CheckCircle2: () => ,
+ Clock: () => ,
+ CornerDownRight: () => ,
+ FileText: () => ,
+ Loader2: () => ,
+ Mail: () => ,
+ Network: () => ,
+ Search: () => ,
+ Sparkles: () => ,
+ X: () => ,
+}));
+
+import { SearchLayout } from "./SearchLayout";
+
+function jsonResponse(body: unknown) {
+ return new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
+type DeferredJsonResponse = {
+ responsePromise: Promise;
+ bodyPromise: Promise;
+ resolveResponse: () => void;
+ resolveBody: (body: unknown) => void;
+};
+
+function deferredJsonResponse(): DeferredJsonResponse {
+ let resolveResponsePromise!: (response: Response) => void;
+ let resolveBodyPromise!: (body: unknown) => void;
+ const bodyPromise = new Promise((resolve) => {
+ resolveBodyPromise = resolve;
+ });
+ const responsePromise = new Promise((resolve) => {
+ resolveResponsePromise = resolve;
+ });
+ const response = new Response(null, {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ vi.spyOn(response, "json").mockImplementation(() => bodyPromise);
+
+ return {
+ responsePromise,
+ bodyPromise,
+ resolveResponse: () => resolveResponsePromise(response),
+ resolveBody: resolveBodyPromise,
+ };
+}
+
+function statusWithText(container: HTMLElement, text: string) {
+ return Array.from(container.querySelectorAll('[role="status"]')).find(
+ (element) => element.textContent?.includes(text),
+ ) ?? null;
+}
+
+describe("SearchLayout empty-result live region", () => {
+ let root: Root | null = null;
+ let container: HTMLDivElement | null = null;
+
+ afterEach(() => {
+ if (root) act(() => root?.unmount());
+ root = null;
+ container?.remove();
+ container = null;
+ vi.unstubAllGlobals();
+ });
+
+ it("announces an empty result only after the asynchronous response body leaves loading", async () => {
+ const searchResponse = deferredJsonResponse();
+
+ vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.endsWith("/api/search/answer")) {
+ return Promise.resolve(jsonResponse({ answer: null, citations: [], provenance: null }));
+ }
+ if (url.endsWith("/api/search")) return searchResponse.responsePromise;
+ throw new Error(`Unexpected fetch: ${url}`);
+ }));
+
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ await act(async () => {
+ root?.render();
+ });
+
+ expect(statusWithText(container, "맥락 검색 결과를 불러오는 중입니다.")).not.toBeNull();
+ expect(statusWithText(container, "맥락 검색 결과가 없습니다.")).toBeNull();
+
+ await act(async () => {
+ searchResponse.resolveResponse();
+ await searchResponse.responsePromise;
+ });
+
+ expect(statusWithText(container, "맥락 검색 결과를 불러오는 중입니다.")).not.toBeNull();
+ expect(statusWithText(container, "맥락 검색 결과가 없습니다.")).toBeNull();
+
+ await act(async () => {
+ searchResponse.resolveBody({ results: [] });
+ await searchResponse.bodyPromise;
+ });
+
+ const emptyStatus = statusWithText(container, "맥락 검색 결과가 없습니다.");
+ expect(emptyStatus).not.toBeNull();
+ expect(emptyStatus?.getAttribute("aria-live")).toBe("polite");
+ expect(container.textContent).not.toContain("맥락 검색 결과를 불러오는 중입니다.");
+ });
+});
diff --git a/frontend/src/components/SearchLayout.tsx b/frontend/src/components/SearchLayout.tsx
index 5cf5ad72f..41a19a915 100644
--- a/frontend/src/components/SearchLayout.tsx
+++ b/frontend/src/components/SearchLayout.tsx
@@ -654,7 +654,7 @@ export function SearchLayout() {
{error}
) : filteredResults.length === 0 ? (
-