Skip to main content
Summary

Laravel 프로젝트는 어디서 시작될까

지난 장에서 우리는 board-app 프로젝트를 만들고 브라우저에서 Laravel 첫 화면을 확인했다. 이제 중요한 질문이 남아 있다.

브라우저가 http://127.0.0.1:8000으로 요청을 보냈을 때, Laravel 프로젝트 안에서는 어떤 일이 일어날까?

이 흐름을 알아야 하는 이유는 단순하다. 에러가 났을 때 어디를 봐야 하는지 판단할 수 있기 때문이다. 화면이 안 나오면 뷰 문제인지, URL이 안 맞으면 라우트 문제인지, 데이터베이스 연결이 실패하면 설정 문제인지 구분할 수 있어야 한다.

Laravel 요청은 큰 흐름으로 보면 다음 순서로 움직인다.

브라우저
  ↓
public/index.php
  ↓
Laravel 애플리케이션 부팅
  ↓
미들웨어
  ↓
라우터
  ↓
컨트롤러 또는 라우트 함수
  ↓
뷰, JSON, 리다이렉트 같은 응답
  ↓
브라우저

실제로는 미들웨어가 라우터 앞뒤에서 여러 번 관여하고, Laravel 내부에서도 더 많은 준비 과정이 일어난다. 하지만 처음부터 모든 세부 단계를 외울 필요는 없다. 이 장에서는 각 단계가 "어떤 책임을 맡는지"에 집중하자.

Laravel 애플리케이션의 구조와 요청 생명주기AI 생성 이미지

모든 웹 요청의 입구, public/index.php

Laravel에서 웹으로 들어오는 요청의 시작점은 public/index.php다. 웹 서버는 프로젝트 전체를 공개하지 않고 보통 public 디렉터리만 공개한다. 이것은 보안상 중요하다. .env, app, config, database 같은 내부 파일이 브라우저에서 직접 열리면 안 되기 때문이다.

public/index.php는 직접 많은 일을 하지 않는다. 핵심 역할은 Laravel 애플리케이션을 깨우고, 현재 HTTP 요청을 넘겨주는 것이다. 공식 요청 생명주기에서도 모든 요청은 먼저 public/index.php로 들어오고, Composer 오토로더를 불러온 뒤 Laravel 애플리케이션 인스턴스를 준비한다고 설명한다.1

간단히 표현하면 다음과 같다.

// public/index.php의 역할을 단순화한 그림
require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$app->handleRequest(Request::capture());

실제 파일 내용은 Laravel 버전에 따라 조금 다를 수 있다. 하지만 의미는 같다.

첫째, vendor/autoload.php를 불러와 Composer가 관리하는 클래스들을 자동으로 찾을 수 있게 한다. 둘째, bootstrap/app.php를 통해 Laravel 애플리케이션을 만든다. 셋째, 브라우저에서 들어온 요청을 Laravel에게 넘긴다.

따라서 public/index.php는 기능을 작성하는 곳이 아니다. 게시글 목록, 로그인, 댓글 작성 같은 코드는 이 파일에 넣지 않는다. 이 파일은 "입구"일 뿐이다.

요청은 어떻게 라우터와 컨트롤러까지 도착할까

Laravel 애플리케이션이 부팅되면 요청은 여러 준비 단계를 거친다. 설정을 읽고, 서비스 프로바이더를 등록하고, 요청을 처리하는 데 필요한 기본 기능을 준비한다. 그다음 현재 요청이 어떤 라우트에 해당하는지 찾고, 필요한 미들웨어를 거쳐 컨트롤러나 라우트 함수로 전달한다.

예를 들어 사용자가 /posts에 접속한다고 해 보자.

GET /posts

이 요청은 먼저 public/index.php를 통과한다. 그다음 Laravel은 현재 요청을 처리할 준비를 한다. 준비가 끝나면 라우터가 routes/web.php 파일에 등록된 URL 목록을 확인한다.

// routes/web.php
use IlluminateSupportFacadesRoute;

Route::get('/posts', function () {
    return '게시글 목록';
});

라우터는 "GET 방식으로 /posts가 들어오면 이 함수를 실행한다"는 규칙을 찾는다. 그리고 함수가 반환한 문자열을 HTTP 응답으로 바꿔 브라우저에 돌려준다.

나중에는 이 함수를 컨트롤러로 분리한다.

// routes/web.php
use AppHttpControllersPostController;
use IlluminateSupportFacadesRoute;

Route::get('/posts', [PostController::class, 'index']);

이제 요청은 PostController의 index 메서드로 이동한다.

// app/Http/Controllers/PostController.php
namespace AppHttpControllers;

class PostController extends Controller
{
    public function index()
    {
        return view('posts.index');
    }
}

컨트롤러는 필요한 데이터를 준비하고, 최종 화면은 보통 Blade 뷰가 담당한다.

<!-- resources/views/posts/index.blade.php -->
<h1>게시글 목록</h1>

이 예제의 흐름은 다음과 같이 정리할 수 있다.

/posts 요청
  ↓
routes/web.php에서 URL 규칙 찾기
  ↓
PostController@index 실행
  ↓
resources/views/posts/index.blade.php 렌더링
  ↓
HTML 응답 반환

앞으로 이 책에서 만들 기능도 이 흐름 위에 하나씩 쌓인다. 라우트는 URL을 연결하고, 컨트롤러는 요청을 처리하며, 뷰는 화면을 만든다.

미들웨어는 요청의 검문소다

미들웨어는 요청이 목적지에 도착하기 전에 검사하거나, 응답이 브라우저로 돌아가기 전에 손볼 수 있는 계층이다. 처음에는 라우터와 컨트롤러 사이에 있는 검문소처럼 이해하면 쉽다. 다만 실제로는 모든 요청에 먼저 적용되는 미들웨어도 있고, 특정 라우트에만 적용되는 미들웨어도 있다.

대표적인 예는 로그인 검사다.

/posts/create 요청
  ↓
로그인했는지 검사
  ↓
로그인했다면 글쓰기 화면 표시
  ↓
로그인하지 않았다면 로그인 페이지로 이동

이처럼 미들웨어는 컨트롤러가 실행되기 전에 요청을 막을 수도 있다. 그래서 "컨트롤러 코드가 실행되지 않는다"면 라우트만 볼 것이 아니라 미들웨어도 함께 확인해야 한다.

Laravel에는 웹 요청을 위한 기본 미들웨어 그룹이 있다. 세션, 쿠키, CSRF 보호 같은 기능이 여기에 포함된다. CSRF는 사용자가 의도하지 않은 폼 요청을 막기 위한 보안 장치다. 폼 전송에서 419 오류가 난다면 라우트나 컨트롤러보다 CSRF 토큰을 먼저 의심해야 할 때가 많다.

예를 들어 POST 요청을 보내는 폼에는 보통 다음처럼 @csrf를 넣는다.

<form method="POST" action="/posts">
    @csrf
    <button type="submit">저장</button>
</form>

이 한 줄이 빠지면 Laravel은 요청이 정상적인 화면에서 온 것인지 확인할 수 없어 419 응답을 돌려줄 수 있다.

주요 디렉터리는 역할로 구분한다

Laravel 프로젝트는 처음 보면 폴더가 많아 복잡해 보인다. 하지만 자주 만지는 곳은 정해져 있다. 폴더 이름보다 "어떤 종류의 코드가 들어가는지"로 기억하면 쉽다.

app

app 디렉터리에는 애플리케이션의 핵심 PHP 코드가 들어간다. 컨트롤러, 모델, 서비스 클래스, 정책, 미들웨어 등 우리가 직접 작성하는 서버 코드의 중심이다.

예를 들어 게시글 데이터를 다루는 Post 모델과 게시글 요청을 처리하는 PostController는 보통 여기에 위치한다.

app/Models/Post.php
app/Http/Controllers/PostController.php

오류 메시지에 App...으로 시작하는 클래스가 보이면 대체로 app 디렉터리 안에서 찾을 수 있다.

routes

routes 디렉터리에는 URL과 기능을 연결하는 파일이 있다.

routes/web.php
routes/console.php

web.php는 브라우저 화면 중심의 웹 요청에 사용한다. 세션과 쿠키를 사용하는 일반 웹 페이지가 여기에 해당한다.

API 요청을 만들 때는 routes/api.php를 사용할 수 있다. 다만 Laravel 11 이후의 기본 프로젝트에서는 api.php가 처음부터 보이지 않을 수 있다. API 라우팅이 필요하면 다음 명령으로 활성화할 수 있다.

php artisan install:api

그러면 routes/api.php가 만들어지고, JSON 응답을 주고받는 API 요청을 별도로 관리할 수 있다. 이 내용은 API를 다루는 장에서 다시 자세히 살펴본다.

"이 URL은 어디에 연결되어 있지?"라는 질문이 생기면 먼저 routes를 확인하자. 라우트 목록을 한눈에 보고 싶을 때는 다음 명령도 도움이 된다.

php artisan route:list

resources

resources에는 화면과 프론트엔드 원본 자원이 들어간다.

resources/views
resources/css
resources/js

Blade 템플릿은 resources/views 아래에 둔다. 컨트롤러에서 view('posts.index')를 반환하면 Laravel은 다음 파일을 찾는다.

resources/views/posts/index.blade.php

즉 점(.)은 디렉터리 구분으로 해석된다.

database

database 디렉터리는 데이터베이스 구조와 테스트 데이터를 관리하는 곳이다.

database/migrations
database/seeders
database/factories

마이그레이션은 테이블 구조를 코드로 기록한다. 예를 들어 posts 테이블을 만들고 싶다면 SQL을 직접 손으로 관리하기보다 마이그레이션 파일을 작성한다. 이 내용은 뒤의 데이터베이스 장에서 자세히 다룬다.

config

config 디렉터리에는 Laravel과 패키지의 설정 파일이 있다.

config/app.php
config/database.php
config/cache.php
config/mail.php

데이터베이스 연결, 메일 서버, 캐시 방식 같은 설정은 이곳에서 확인한다. 하지만 비밀번호나 환경마다 달라지는 값은 보통 직접 적지 않고 .env에서 가져온다.

서비스 컨테이너와 서비스 프로바이더는 연결 장치다

Laravel을 배우다 보면 "서비스 컨테이너"와 "서비스 프로바이더"라는 말을 만나게 된다. 처음부터 깊게 알 필요는 없지만, 요청 생명주기를 이해하려면 기본 이미지는 잡아 두는 편이 좋다.

서비스 컨테이너는 객체를 만들고 연결해 주는 저장소이자 조립 장치다. 컨트롤러가 어떤 클래스를 필요로 할 때 Laravel은 컨테이너를 통해 그 객체를 만들 수 있다.

예를 들어 나중에 게시글 관련 로직을 PostService라는 클래스로 분리했다고 하자.

class PostController extends Controller
{
    public function __construct(
        private PostService $posts
    ) {}
}

우리가 직접 new PostService()를 쓰지 않아도 Laravel은 타입 정보를 보고 필요한 객체를 주입할 수 있다. 이런 방식을 의존성 주입이라고 한다. 의존성은 "이 클래스가 일하기 위해 필요한 다른 객체"라는 뜻이다.

서비스 프로바이더는 이런 연결 규칙과 초기화 작업을 등록하는 곳이다. Laravel 공식 문서는 서비스 프로바이더를 애플리케이션 부팅의 중심 위치로 설명한다. 컨테이너 바인딩, 이벤트 리스너 등록, 패키지 설정처럼 애플리케이션이 실행되기 전에 준비해야 할 많은 작업이 프로바이더를 통해 처리된다.2

현대 Laravel에서는 사용자 정의 프로바이더가 보통 app/Providers에 있고, 등록 목록은 bootstrap/providers.php에서 관리된다.

app/Providers/AppServiceProvider.php
bootstrap/providers.php

프로바이더에는 주로 두 메서드가 있다.

public function register(): void
{
    // 컨테이너에 객체 생성 규칙을 등록
}

public function boot(): void
{
    // 모든 등록이 끝난 뒤 실행할 초기화 작업
}

초보 단계에서는 AppServiceProvider를 자주 수정하지 않는다. 다만 Laravel 내부 기능이 "어딘가에서 자동으로 연결된다"는 느낌이 들 때, 그 중심에 컨테이너와 프로바이더가 있다는 점을 기억하면 된다.

config와 .env는 함께 동작한다

Laravel 설정은 config 파일과 .env 파일이 함께 만든다.

.env는 현재 실행 환경의 값을 담는다.

APP_NAME="Board App"
APP_ENV=local
APP_DEBUG=true

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=board_app
DB_USERNAME=root
DB_PASSWORD=

config/database.php 같은 설정 파일은 이 값을 읽어 사용한다.

'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
],

여기서 중요한 원칙이 있다. 애플리케이션 코드에서는 가능하면 env()를 직접 자주 호출하지 말고, 설정 파일을 통해 읽는 편이 안전하다.

예를 들어 코드 곳곳에서 env('SOME_VALUE')를 직접 쓰면 설정 캐시를 사용할 때 예상과 다르게 동작할 수 있다. 일반 코드에서는 다음처럼 config()를 사용하는 습관을 들이자.

$name = config('app.name');

환경별 설정 원칙도 중요하다.

로컬 개발 환경: APP_ENV=local, APP_DEBUG=true
운영 서버: APP_ENV=production, APP_DEBUG=false

운영 서버에서 APP_DEBUG=true로 두면 오류 화면에 내부 경로, 설정 정보, 쿼리 등이 노출될 수 있다. 개발할 때는 편리하지만 실제 서비스에서는 위험하다.

또한 .env 파일은 Git에 올리지 않는다. 대신 .env.example에 필요한 키 이름만 공유한다. 각 개발자와 서버는 자기 환경에 맞는 .env를 따로 만든다.

에러가 날 때는 흐름을 거꾸로 추적한다

요청 생명주기를 이해하면 디버깅 순서가 정리된다. 문제가 생겼을 때 무작정 모든 파일을 열지 않아도 된다.

URL을 찾을 수 없다는 404 오류가 나면 먼저 routes/web.php를 확인한다.

요청 URL이 맞는가?
HTTP 메서드가 GET인지 POST인지 맞는가?
라우트 경로에 오타가 없는가?

컨트롤러 클래스나 메서드를 찾을 수 없다는 오류가 나면 app/Http/Controllers와 네임스페이스를 확인한다.

파일명이 클래스명과 맞는가?
namespace가 AppHttpControllers로 되어 있는가?
routes/web.php에서 use 문을 제대로 썼는가?
라우트에 적은 메서드 이름이 실제 컨트롤러에 있는가?

뷰를 찾을 수 없다는 오류가 나면 resources/views를 확인한다.

view('posts.index')라면
resources/views/posts/index.blade.php 파일이 있는가?
파일명에 오타가 없는가?

폼 전송 뒤 419 오류가 나면 CSRF 토큰을 확인한다.

<form> 안에 @csrf가 있는가?
요청이 web 라우트로 들어오고 있는가?
세션이 정상적으로 동작하는가?

데이터베이스 연결 오류가 나면 .env와 config/database.php를 확인한다.

DB_CONNECTION 값이 맞는가?
DB_DATABASE 값이 맞는가?
DB_USERNAME, DB_PASSWORD가 맞는가?
데이터베이스 서버가 실행 중인가?

로그는 storage/logs/laravel.log에서 확인할 수 있다. 브라우저 화면의 오류 메시지만 보지 말고 로그 파일도 함께 보는 습관을 들이자.

이번 장에서 기억할 것

Laravel 애플리케이션은 public/index.php에서 시작하지만, 실제 기능은 여러 계층에 나뉘어 있다. 라우트는 URL을 찾고, 미들웨어는 요청을 검사하며, 컨트롤러는 처리 흐름을 담당하고, 뷰는 화면을 만든다.

디렉터리도 역할별로 나뉜다. app은 핵심 PHP 코드, routes는 URL 연결, resources는 화면과 프론트엔드 자원, database는 데이터베이스 구조, config는 설정을 담당한다.

서비스 컨테이너와 서비스 프로바이더는 Laravel 내부 기능과 애플리케이션 코드를 연결하는 기반이다. 지금은 깊게 다루지 않아도 된다. 다만 컨트롤러가 실행되기 전에 Laravel이 설정을 읽고, 필요한 서비스를 등록하고, 요청을 처리할 준비를 끝낸다는 점은 기억하자.

다음 장에서는 이 흐름 중 첫 번째 실전 지점인 라우팅을 자세히 다룬다. URL 하나가 어떤 코드와 연결되는지 직접 작성하면서 Laravel 애플리케이션의 입구를 더 명확히 이해하게 될 것이다.

참고

1Laravel 12.x Request Lifecycle

2Laravel 12.x Service Providers

책 목록

라라벨 학습서: 웹 기초부터 배포까지 배우는 Laravel 실전 입문

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기