본문 바로가기

HTTP 응답

업데이트됨

번역일: 2026년 10월 8일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 10월 7일
번역 갱신
2026년 10월 8일

HTTP 응답

응답 생성

문자열과 배열

모든 라우트와 컨트롤러는 브라우저에 보낼 응답을 반환해야 합니다. 가장 기본적인 방법은 문자열을 반환하는 것입니다. Laravel이 완전한 HTTP 응답으로 자동 변환합니다:

Route::get('/', function () { return 'Hello World'; });

배열을 반환하면 JSON 응답으로 자동 변환합니다:

Route::get('/', function () { return [1, 2, 3]; });

NOTE

Eloquent 컬렉션도 라우트나 컨트롤러에서 반환할 수 있으며 JSON으로 자동 변환됩니다.

응답 객체

실제로는 단순 문자열이나 배열보다 Illuminate\Http\Response 인스턴스 또는 뷰를 반환하는 경우가 많습니다.

Response 인스턴스는 HTTP 상태 코드와 헤더를 조정할 수 있게 합니다. HTTP 응답 구성 메서드를 제공하는 Symfony\Component\HttpFoundation\Response를 상속합니다:

Route::get('/home', function () { return response('Hello World', 200) ->header('Content-Type', 'text/plain'); });

Eloquent 모델과 컬렉션

Eloquent ORM 모델과 컬렉션도 직접 반환할 수 있습니다. Laravel은 모델의 숨김 속성을 적용하여 JSON 응답으로 변환합니다:

use App\Models\User; Route::get('/user/{user}', function (User $user) { return $user; });

헤더 추가

대부분의 응답 메서드는 연결해서 호출할 수 있습니다. 예를 들어 header를 연결하여 전송 전에 여러 헤더를 추가합니다:

return response($content) ->header('Content-Type', $type) ->header('X-Header-One', 'Header Value') ->header('X-Header-Two', 'Header Value');

withHeaders로 헤더 배열을 지정할 수도 있습니다:

return response($content) ->withHeaders([ 'Content-Type' => $type, 'X-Header-One' => 'Header Value', 'X-Header-Two' => 'Header Value', ]);

withoutHeader로 특정 헤더를 제거할 수 있습니다:

return response($content)->withoutHeader('X-Debug'); return response($content)->withoutHeader(['X-Debug', 'X-Powered-By']);

캐시 제어 미들웨어

cache.headers로 라우트 그룹의 Cache-Control을 쉽게 설정할 수 있습니다. 지시어를 snake_case로 작성하고 세미콜론으로 구분합니다. etag가 있으면 응답 내용의 xxh128 해시를 ETag 식별자로 자동 설정합니다:

Route::middleware('cache.headers:public;max_age=30;s_maxage=300;stale_while_revalidate=600;etag')->group(function () { Route::get('/privacy', function () { // ... }); Route::get('/terms', function () { // ... }); });

쿠키 추가

Illuminate\Http\Response의 cookie로 쿠키를 추가합니다. 이름, 값, 분 단위 유효 기간을 전달합니다:

return response('Hello World')->cookie( 'name', 'value', $minutes );

자주 사용하지 않는 추가 인수도 받습니다. 일반적으로 PHP의 setcookie 인수와 목적 및 의미가 같습니다:

return response('Hello World')->cookie( 'name', 'value', $minutes, $path, $domain, $secure, $httpOnly );

응답 인스턴스가 아직 없다면 Cookie 파사드로 추가할 쿠키를 예약할 수 있습니다. queue에 생성 인수를 전달하면 브라우저로 응답을 보내기 전에 추가됩니다:

use Illuminate\Support\Facades\Cookie; Cookie::queue('name', 'value', $minutes);

전역 cookie 헬퍼로 나중에 응답에 추가할 Symfony\Component\HttpFoundation\Cookie를 생성할 수 있습니다. 응답에 추가하지 않으면 클라이언트로 전송되지 않습니다:

$cookie = cookie('name', 'value', $minutes); return response('Hello World')->cookie($cookie);

쿠키 조기 만료

응답의 withoutCookie나 withoutCookies로 만료시켜 제거할 수 있습니다:

return response('Hello World')->withoutCookie('name'); return response('Hello World')->withoutCookies([ 'name', 'email', 'preferences', ]);

응답 인스턴스가 아직 없다면 Cookie 파사드의 expire를 사용합니다:

Cookie::expire('name');

쿠키와 암호화

기본적으로 Illuminate\Cookie\Middleware\EncryptCookies가 Laravel이 생성한 모든 쿠키를 암호화하고 서명하여 클라이언트가 읽거나 수정하지 못하게 합니다. 일부 쿠키를 제외하려면 bootstrap/app.php의 encryptCookies를 사용합니다:

->withMiddleware(function (Middleware $middleware): void { $middleware->encryptCookies(except: [ 'cookie_name', ]); })

NOTE

일반적으로 쿠키 암호화를 비활성화하면 안 됩니다. 클라이언트에서 데이터가 노출되거나 변조될 위험이 있습니다.

리다이렉트

Illuminate\Http\RedirectResponse는 다른 URL로 이동하는 데 필요한 헤더를 포함합니다. 가장 간단한 생성 방법은 전역 redirect입니다:

Route::get('/dashboard', function () { return redirect('/home/dashboard'); });

제출한 폼이 유효하지 않은 경우 등 이전 위치로 돌아가려면 전역 back을 사용합니다. 세션을 사용하므로 해당 라우트는 web 미들웨어 그룹을 사용해야 합니다:

Route::post('/user/profile', function () { // Validate the request... return back()->withInput(); });

이름 있는 라우트로 이동

인수 없이 redirect를 호출하면 Illuminate\Routing\Redirector가 반환됩니다. 예를 들어 route로 이름 있는 라우트에 대한 리다이렉트를 생성합니다:

return redirect()->route('login');

라우트 매개변수는 route의 두 번째 인수로 전달합니다:

// For a route with the following URI: /profile/{id} return redirect()->route('profile', ['id' => 1]);

Eloquent 모델로 매개변수 지정

모델의 ID를 라우트 매개변수로 사용할 때 모델 자체를 전달하면 ID를 자동 추출합니다:

// For a route with the following URI: /profile/{id} return redirect()->route('profile', [$user]);

값을 변경하려면 라우트 정의에 컬럼을 지정하거나(/profile/{id:slug}) 모델의 getRouteKey를 재정의합니다:

/** * Get the value of the model's route key. */ public function getRouteKey(): mixed { return $this->slug; }

컨트롤러 액션으로 이동

컨트롤러 액션으로 이동하려면 action에 컨트롤러와 액션 이름을 전달합니다:

use App\Http\Controllers\UserController; return redirect()->action([UserController::class, 'index']);

매개변수는 action의 두 번째 인수로 전달합니다:

return redirect()->action( [UserController::class, 'profile'], ['id' => 1] );

외부 도메인으로 이동

외부 도메인으로 이동하려면 away를 사용합니다. 추가 URL 인코딩이나 검증 없이 RedirectResponse를 생성합니다:

return redirect()->away('https://www.google.com');

플래시 세션 데이터와 함께 이동

리다이렉트와 세션 플래시 데이터는 보통 함께 사용합니다. 성공 후 메시지를 저장하는 경우가 대표적입니다. 메서드를 연결하여 응답 생성과 데이터 저장을 한 번에 할 수 있습니다:

Route::post('/user/profile', function () { // ... return redirect('/dashboard')->with('status', 'Profile updated!'); });

이동 후 세션에서 메시지를 읽어 표시합니다. Blade 예제입니다:

@if (session('status')) <div class="alert alert-success"> {{ session('status') }} </div> @endif

입력값과 함께 이동

RedirectResponse의 withInput으로 현재 입력값을 세션에 플래시 저장한 뒤 이동할 수 있습니다. 보통 검증 오류 시 사용합니다. 다음 요청에서 이전 입력 조회로 폼을 다시 채울 수 있습니다:

return back()->withInput();

기타 응답 유형

response를 인수 없이 호출하면 Illuminate\Contracts\Routing\ResponseFactory 계약의 구현체가 반환됩니다. 여러 응답 생성 메서드를 제공합니다.

뷰 응답

뷰를 반환하면서 상태 코드와 헤더도 조정하려면 view 메서드를 사용합니다:

return response() ->view('hello', $data, 200) ->header('Content-Type', $type);

별도 상태 코드나 헤더가 필요하지 않으면 전역 view 헬퍼를 사용합니다.

JSON 응답

json은 Content-Type을 application/json으로 설정하고 PHP의 json_encode로 배열을 변환합니다:

return response()->json([ 'name' => 'Abigail', 'state' => 'CA', ]);

JSONP는 json과 withCallback을 함께 사용합니다:

return response() ->json(['name' => 'Abigail', 'state' => 'CA']) ->withCallback($request->input('callback'));

Markdown 응답

markdown은 Content-Type을 text/markdown으로 설정해 내용을 반환합니다:

return response()->markdown("# Hello\n\nWorld");

두 번째와 세 번째 인수로 HTTP 상태 코드와 추가 헤더 배열을 전달할 수 있습니다:

return response()->markdown('# Not Found', 404, ['X-Custom-Header' => 'Value']);

파일 다운로드

download는 지정한 파일을 브라우저가 다운로드하도록 응답을 생성합니다. 두 번째 인수는 사용자에게 보일 파일 이름이고 세 번째는 HTTP 헤더 배열입니다:

return response()->download($pathToFile); return response()->download($pathToFile, $name, $headers);

WARNING

파일 다운로드를 관리하는 Symfony HttpFoundation은 ASCII 파일 이름을 요구합니다.

파일 응답

file은 이미지나 PDF를 다운로드 대신 브라우저에 직접 표시합니다. 첫 인수는 절대 경로, 두 번째는 헤더 배열입니다:

return response()->file($pathToFile); return response()->file($pathToFile, $headers);

스트리밍 응답

생성되는 데이터를 즉시 전송하면 큰 응답에서 메모리 사용을 줄이고 성능을 개선할 수 있습니다. 서버 전송이 끝나기 전에 클라이언트가 처리를 시작할 수 있습니다:

Route::get('/stream', function () { return response()->stream(function (): void { foreach (['developer', 'admin'] as $string) { echo $string; ob_flush(); flush(); sleep(2); // Simulate delay between chunks... } }, 200, ['X-Accel-Buffering' => 'no']); });

stream에 전달한 클로저가 Generator를 반환하면 Laravel이 각 문자열 사이에서 출력 버퍼를 자동 비우고 Nginx 출력 버퍼링도 비활성화합니다:

Route::post('/chat', function () { return response()->stream(function (): Generator { $stream = OpenAI::client()->chat()->createStreamed(...); foreach ($stream as $response) { yield $response->choices[0]; } }); });

스트리밍 응답 사용

Laravel의 stream npm 패키지는 응답 및 이벤트 스트림을 처리할 API를 제공합니다. 먼저 @laravel/stream-react, @laravel/stream-vue, @laravel/stream-svelte 중 필요한 패키지를 설치합니다:

React

npm install @laravel/stream-react

Vue

npm install @laravel/stream-vue

Svelte

npm install @laravel/stream-svelte

useStream에 URL을 전달하면 Laravel에서 내용이 반환될 때마다 응답을 이어 붙여 data를 자동 갱신합니다:

React

import { useStream } from "@laravel/stream-react"; function App() { const { data, isFetching, isStreaming, send } = useStream("chat"); const sendMessage = () => { send({ message: `Current timestamp: ${Date.now()}`, }); }; return ( <div> <div>{data}</div> {isFetching && <div>Connecting...</div>} {isStreaming && <div>Generating...</div>} <button onClick={sendMessage}>Send Message</button> </div> ); }

Vue

<script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; const { data, isFetching, isStreaming, send } = useStream("chat"); const sendMessage = () => { send({ message: `Current timestamp: ${Date.now()}`, }); }; </script> <template> <div> <div>{{ data }}</div> <div v-if="isFetching">Connecting...</div> <div v-if="isStreaming">Generating...</div> <button @click="sendMessage">Send Message</button> </div> </template>

Svelte

<script> import { useStream } from "@laravel/stream-svelte"; const stream = useStream("chat"); const sendMessage = () => { stream.send({ message: `Current timestamp: ${Date.now()}`, }); }; </script> <div> <div>{$stream.data}</div> {#if $stream.isFetching} <div>Connecting...</div> {/if} {#if $stream.isStreaming} <div>Generating...</div> {/if} <button onclick={sendMessage}>Send Message</button> </div>

send로 데이터를 보내면 새 데이터를 전송하기 전에 활성 스트림 연결을 취소합니다. 모든 요청은 JSON POST입니다.

WARNING

useStream은 POST 요청을 보내므로 유효한 CSRF 토큰이 필요합니다. 가장 간단한 방법은 레이아웃 head에 meta 태그로 포함하는 것입니다.

두 번째 인수인 옵션 객체로 동작을 조정할 수 있습니다. 기본값은 다음과 같습니다:

React

import { useStream } from "@laravel/stream-react"; function App() { const { data } = useStream("chat", { id: undefined, initialInput: undefined, headers: undefined, csrfToken: undefined, onResponse: (response: Response) => void, onData: (data: string) => void, onCancel: () => void, onFinish: () => void, onError: (error: Error) => void, }); return <div>{data}</div>; }

Vue

<script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; const { data } = useStream("chat", { id: undefined, initialInput: undefined, headers: undefined, csrfToken: undefined, onResponse: (response: Response) => void, onData: (data: string) => void, onCancel: () => void, onFinish: () => void, onError: (error: Error) => void, }); </script> <template> <div>{{ data }}</div> </template>

Svelte

<script> import { useStream } from "@laravel/stream-svelte"; const stream = useStream("chat", { id: undefined, initialInput: undefined, headers: undefined, csrfToken: undefined, onResponse: (response) => {}, onData: (data) => {}, onCancel: () => {}, onFinish: () => {}, onError: (error) => {}, }); </script> <div>{$stream.data}</div>

onResponse는 최초 응답 성공 후 호출되며 원시 Response를 받습니다. onData는 청크를 받을 때마다 현재 청크를 받습니다. onFinish는 스트림 완료 시와 가져오기·읽기 중 오류가 발생했을 때 호출됩니다.

기본적으로 초기화만으로 요청하지 않습니다. initialInput으로 초기 페이로드를 지정할 수 있습니다:

React

import { useStream } from "@laravel/stream-react"; function App() { const { data } = useStream("chat", { initialInput: { message: "Introduce yourself.", }, }); return <div>{data}</div>; }

Vue

<script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; const { data } = useStream("chat", { initialInput: { message: "Introduce yourself.", }, }); </script> <template> <div>{{ data }}</div> </template>

Svelte

<script> import { useStream } from "@laravel/stream-svelte"; const stream = useStream("chat", { initialInput: { message: "Introduce yourself.", }, }); </script> <div>{$stream.data}</div>

수동 취소는 훅이 반환한 cancel을 사용합니다:

React

import { useStream } from "@laravel/stream-react"; function App() { const { data, cancel } = useStream("chat"); return ( <div> <div>{data}</div> <button onClick={cancel}>Cancel</button> </div> ); }

Vue

<script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; const { data, cancel } = useStream("chat"); </script> <template> <div> <div>{{ data }}</div> <button @click="cancel">Cancel</button> </div> </template>

Svelte

<script> import { useStream } from "@laravel/stream-svelte"; const stream = useStream("chat"); </script> <div> <div>{$stream.data}</div> <button onclick={() => stream.cancel()}>Cancel</button> </div>

useStream을 사용할 때마다 임의의 id가 생성되고 매 요청의 X-STREAM-ID 헤더로 서버에 전송됩니다. 여러 컴포넌트에서 같은 스트림을 읽고 쓰려면 자신의 id를 전달합니다:

React

// App.tsx import { useStream } from "@laravel/stream-react"; function App() { const { data, id } = useStream("chat"); return ( <div> <div>{data}</div> <StreamStatus id={id} /> </div> ); } // StreamStatus.tsx import { useStream } from "@laravel/stream-react"; function StreamStatus({ id }) { const { isFetching, isStreaming } = useStream("chat", { id }); return ( <div> {isFetching && <div>Connecting...</div>} {isStreaming && <div>Generating...</div>} </div> ); }

Vue

<!-- App.vue --> <script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; import StreamStatus from "./StreamStatus.vue"; const { data, id } = useStream("chat"); </script> <template> <div> <div>{{ data }}</div> <StreamStatus :id="id" /> </div> </template> <!-- StreamStatus.vue --> <script setup lang="ts"> import { useStream } from "@laravel/stream-vue"; const props = defineProps<{ id: string; }>(); const { isFetching, isStreaming } = useStream("chat", { id: props.id }); </script> <template> <div> <div v-if="isFetching">Connecting...</div> <div v-if="isStreaming">Generating...</div> </div> </template>

Svelte

<!-- App.svelte --> <script> import { useStream } from "@laravel/stream-svelte"; import StreamStatus from "./StreamStatus.svelte"; const stream = useStream("chat"); </script> <div> <div>{$stream.data}</div> <StreamStatus id={stream.id} /> </div> <!-- StreamStatus.svelte --> <script> import { useStream } from "@laravel/stream-svelte"; let { id } = $props(); const stream = useStream("chat", { id }); </script> <div> {#if $stream.isFetching} <div>Connecting...</div> {/if} {#if $stream.isStreaming} <div>Generating...</div> {/if} </div>

JSON 스트리밍 응답

JSON 데이터를 조금씩 전송하려면 streamJson을 사용합니다. 큰 데이터셋을 JavaScript가 쉽게 파싱할 형식으로 점진적으로 브라우저에 보낼 때 유용합니다:

use App\Models\User; Route::get('/users.json', function () { return response()->streamJson([ 'users' => User::cursor(), ]); });

useJsonStream은 스트리밍 완료 후 JSON 파싱을 시도한다는 점 외에는 useStream과 같습니다:

React

import { useJsonStream } from "@laravel/stream-react"; type User = { id: number; name: string; email: string; }; function App() { const { data, send } = useJsonStream<{ users: User[] }>("users"); const loadUsers = () => { send({ query: "taylor", }); }; return ( <div> <ul> {data?.users.map((user) => ( <li> {user.id}: {user.name} </li> ))} </ul> <button onClick={loadUsers}>Load Users</button> </div> ); }

Vue

<script setup lang="ts"> import { useJsonStream } from "@laravel/stream-vue"; type User = { id: number; name: string; email: string; }; const { data, send } = useJsonStream<{ users: User[] }>("users"); const loadUsers = () => { send({ query: "taylor", }); }; </script> <template> <div> <ul> <li v-for="user in data?.users" :key="user.id"> {{ user.id }}: {{ user.name }} </li> </ul> <button @click="loadUsers">Load Users</button> </div> </template>

Svelte

<script> import { useJsonStream } from "@laravel/stream-svelte"; const stream = useJsonStream("users"); const loadUsers = () => { stream.send({ query: "taylor", }); }; </script> <div> <ul> {#if $stream.data?.users} {#each $stream.data.users as user (user.id)} <li>{user.id}: {user.name}</li> {/each} {/if} </ul> <button onclick={loadUsers}>Load Users</button> </div>

이벤트 스트림(SSE)

eventStream은 text/event-stream으로 SSE 응답을 반환합니다. 전달한 클로저는 응답이 준비될 때마다 스트림에 yield해야 합니다:

Route::get('/chat', function () { return response()->eventStream(function () { $stream = OpenAI::client()->chat()->createStreamed(...); foreach ($stream as $response) { yield $response->choices[0]; } }); });

이벤트 이름을 변경하려면 StreamedEvent 인스턴스를 yield합니다:

use Illuminate\Http\StreamedEvent; yield new StreamedEvent( event: 'update', data: $response->choices[0], );

이벤트 스트림 사용

Laravel의 stream npm 패키지는 이벤트 스트림 처리를 위한 API를 제공합니다. 필요한 React, Vue, Svelte 패키지를 설치합니다:

React

npm install @laravel/stream-react

Vue

npm install @laravel/stream-vue

Svelte

npm install @laravel/stream-svelte

useEventStream에 URL을 전달하면 메시지가 도착할 때마다 응답을 이어 붙여 message를 자동 갱신합니다:

React

import { useEventStream } from "@laravel/stream-react"; function App() { const { message } = useEventStream("/chat"); return <div>{message}</div>; }

Vue

<script setup lang="ts"> import { useEventStream } from "@laravel/stream-vue"; const { message } = useEventStream("/chat"); </script> <template> <div>{{ message }}</div> </template>

Svelte

<script> import { useEventStream } from "@laravel/stream-svelte"; const eventStream = useEventStream("/chat"); </script> <div>{$eventStream.message}</div>

두 번째 인수의 옵션 객체로 동작을 조정할 수 있습니다. 기본값은 다음과 같습니다:

React

import { useEventStream } from "@laravel/stream-react"; function App() { const { message } = useEventStream("/stream", { eventName: "update", onMessage: (message) => { // }, onError: (error) => { // }, onComplete: () => { // }, endSignal: "</stream>", glue: " ", }); return <div>{message}</div>; }

Vue

<script setup lang="ts"> import { useEventStream } from "@laravel/stream-vue"; const { message } = useEventStream("/chat", { eventName: "update", onMessage: (message) => { // ... }, onError: (error) => { // ... }, onComplete: () => { // ... }, endSignal: "</stream>", glue: " ", }); </script>

Svelte

<script> import { useEventStream } from "@laravel/stream-svelte"; const eventStream = useEventStream("/chat", { eventName: "update", onMessage: (event) => { // }, onError: (error) => { // }, onComplete: () => { // }, endSignal: "</stream>", glue: " ", replace: false, }); </script>

프런트엔드에서 EventSource로 직접 처리할 수도 있습니다. eventStream은 완료 시 </stream> 업데이트를 자동 전송합니다:

const source = new EventSource('/chat'); source.addEventListener('update', (event) => { if (event.data === '</stream>') { source.close(); return; } console.log(event.data); });

마지막 이벤트를 변경하려면 endStreamWith 인수로 StreamedEvent를 전달합니다:

return response()->eventStream(function () { // ... }, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));

스트리밍 다운로드

작업 결과 문자열을 디스크에 저장하지 않고 다운로드 응답으로 만들려면 streamDownload를 사용합니다. 콜백, 파일 이름, 선택적인 헤더 배열을 받습니다:

use App\Services\GitHub; return response()->streamDownload(function () { echo GitHub::api('repo') ->contents() ->readme('laravel', 'laravel')['contents']; }, 'laravel-readme.md');

응답 매크로

여러 라우트와 컨트롤러에서 재사용할 응답은 Response 파사드의 macro로 정의할 수 있습니다. 보통 App\Providers\AppServiceProvider 등 서비스 프로바이더의 boot에서 호출합니다:

<?php namespace App\Providers; use Illuminate\Support\Facades\Response; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot(): void { Response::macro('caps', function (string $value) { return Response::make(strtoupper($value)); }); } }

macro는 첫 번째 인수로 이름, 두 번째로 클로저를 받습니다. ResponseFactory 구현체나 response 헬퍼에서 해당 이름으로 호출하면 클로저가 실행됩니다:

return response()->caps('foo');

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 10월 8일