Laravel, CRUD 글 수정 기능 구현

포스트 썸네일 이미지

이전 포스트에서는 잠깐 쉬어가는 느낌으로 라라벨 개발에 필요한 VSCode 필수 익스텐션을 소개했다.

오늘은 사용자가 작성한 글을 수정하는 기능을 구현해 보려고 한다.

참고로 글 수정 기능을 완벽하게 구현하기 위해서는 이어지는 다음 포스트들도 확인해야 한다.




이전 포스트




전체적인 코드 수정은 이 링크를 통해서 확인할 수 있다.




routes/web.php에서 라우팅 설정하기


routes/web.php의 수정 전 코드

Route::resource('posts', PostController::class)
    ->only(['index', 'store'])
    ->middleware(['auth', 'verified']);

routes/web.php의 수정 전 코드다.

이 부분을 아래와 같이 수정할 거다.




routes/web.php의 수정 후 코드

Route::resource('posts', PostController::class)
    ->only(['index', 'store', 'edit', 'update'])
    ->middleware(['auth', 'verified']);

기존에는 2개의 문만 열어두었던 리소스 라우터에 editupdate도 추가로 개방했다.




Verb URI Action Route Name
GET /posts index posts.index
POST /posts store posts.store
GET /posts/{post}/edit edit posts.edit
PUT/PATCH /posts/{post} update posts.update

이 설정 덕분에 라라벨은 내부적으로 이 표의 하단의 2개의 HTTP 요청 주소를 자동으로 인식하고 컨트롤러와 연결할 수 있게 된다.




위의 표의 상단 2개의 HTTP 요청 주소는 이 포스트에서 연결했었다.





index 페이지에 드롭다운 메뉴 컴포넌트 추가


resources/views/posts/index.blade.php에 추가할 코드 1

@unless ($post->created_at->eq($post->updated_at))
    <small class="text-sm text-gray-600"> &middot; {{ __('edited') }}</small>
@endunless

</time> 태그 바로 아래에 이 코드를 추가하자.


이 코드는 사용자가 작성한 글이 '최초 작성 이후에 수정된 적이 있는가?'를 판별하여, 수정되었다면 화면에 'edited'라고 표시해 주는 백엔드 조건부 렌더링 문법이다.


@unless (...)는 라라벨 블레이드 뷰 엔진이 제공하는 부정 조건문 지시어다.

php의 if (!...)문과 완벽하게 동일하게 작동한다.


$post->created_at은 데이터베이스 테이블에 글이 처음 저장될 때, $post->updated_at은 내용이 변경되어 업데이트될 때 타임스탬프가 자동으로 찍히는 카본Carbon 날짜 객체들이다.


Equals (->eq(...))는 카본 라이브러리가 제공하는 날짜 비교 메서드다.

두 날짜 객체의 시각이 밀리초 단위까지 완벽하게 일치하는지 검사하여 true 또는 false를 반환한다.

처음 글을 썼을 때는 생성일과 수정일이 완전히 같으므로 eq()의 결과는 true가 된다.

글을 한 번이라도 수정하면 updated_at 시각이 수정돼서 eq() 결과는 false가 된다.




resources/views/posts/index.blade.php에 추가할 코드 2

<x-dropdown>
    <x-slot name="trigger">
        <button>
            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
                <path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
            </svg>
        </button>
    </x-slot>
    <x-slot name="content">
        <x-dropdown-link :href="route('posts.edit', $post)">
            {{ __('Edit') }}
        </x-dropdown-link>
    </x-slot>
</x-dropdown>

이 코드를 추가해야 할 위치는 설명하기 애매하니, 깃허브 링크를 참고하자.


라라벨 브리즈 패키지가 제공하는 블레이드 컴포넌트(<x-dropdown>)를 활용하여 점 3개(...) 아이콘을 클릭하면 드롭다운 메뉴가 생기도록 했다.

참고로 이렇게 점이 가로로 3개 배열된 모양의 메뉴를 '미트볼 메뉴'라고 한다. (세로로 3개가 배열된 건 '케밥 메뉴')


:href="route('posts.edit', $post)" 부분은 단순히 주소만 넘기는 것이 아니라, 현재 반복문으로 돌고 있는 특정 게시글 객체($post)를 통째로 인자로 주입했다.

라라벨은 이것을 해석해 주소창에 posts/3/edit 같은 형태로 해당 글의 고유 ID값을 알아서 동적으로 바인딩해 준다.




미트볼 메뉴를 클릭하면 수정 버튼이 나타난다

여기까지 수정하면 이렇게 글마다 미트볼 메뉴가 생기고, 클릭하면 수정 버튼이 나타난다.

하지만 아직 수정 페이지를 만들지 않았기 때문에 클릭하면 아무것도 없는 페이지로 이동하게 된다.





edit.blade.php 파일 만들기


posts/edit.blade.php

<x-app-layout>
    <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
        <form method="POST" action="{{ route('posts.update', $post) }}">
            @csrf
            @method('patch')
            <textarea
                name="message"
                class="block w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"
            >{{ old('message', $post->message) }}</textarea>
            <x-input-error :messages="$errors->get('message')" class="mt-2" />
            <div class="mt-4 space-x-2">
                <x-primary-button>{{ __('Save') }}</x-primary-button>
                <a href="{{ route('posts.index') }}">{{ __('Cancel') }}</a>
            </div>
        </form>
    </div>
</x-app-layout>

resources/views/posts 폴더에 edit.blade.php 파일을 만들자.


  • action="{{ route('posts.update', $post) }}": 폼 데이터가 최종적으로 전송될 목적지 주소다. 몇 번 글을 수정할 것인지 명시해야 하므로 브라우저에게 /posts/글ID 주소로 수정 요청(Request)을 보내도록 경로를 지정해 주는 것이다.
  • @method('patch'): HTML <form> 태그는 역사적・기술적 한계로 오직 GET과 POST 방식만 전송할 수 있다. 하지만 현대 RESTful API 표준에서 '기존 데이터의 일부 수정'은 PATCH나 PUT 메서드를 쓰는 것이 원칙이다. 이걸 해결하기 위해 라라벨의 이 지시어를 적어주면, 화면에는 보이지 않는 숨겨진 input 태그(<input type="hidden" name="_method" value="PATCH">)가 생성된다. 브라우저가 POST로 데이터를 보내면, 라라벨 엔진이 이 숨겨진 값을 감지해서, POST로 들어온 요청을 백엔드 내부에서 PATCH 요청으로 전환하여 처리해 준다."
  • {{ old('message', $post->message) }}: old() 함수의 두 번째 인자에 기존 데이터($post->message)를 넣어두면 폼이 처음 열릴 때는 원래 글 내용이 들어온다. 만약 수정을 시도하다가 글자 수 초과 등으로 유효성 검사에 실패하면 방금 전까지 입력했던 수정 시도 글(old('message'))을 최우선으로 복구해 준다.




이렇게 해서 글 수정 페이지도 만들어졌다.

하지만 작은 문제가 있다.

내가 지금 어느 메뉴에 있는지 강조(Active)해 주는 시각적 효과가 사라져 있다.





layouts/navigation.blade.php를 수정해서 활성화(Active) 상태 유지하기


layouts/navigation.blade.php의 수정 전 코드

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
        {{ __('Dashboard') }}
    </x-nav-link>
    <x-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
        {{ __('Posts') }}
    </x-nav-link>
</div>

{{-- 생략 --}}

<div class="pt-2 pb-3 space-y-1">
    <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
        {{ __('Dashboard') }}
    </x-responsive-nav-link>
    <x-responsive-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
        {{ __('Posts') }}
    </x-responsive-nav-link>
</div>

PC와 모바일 화면용 내비게이션 메뉴에 각각 :active="request()->routeIs('posts.index')" 이 부분을 수정할 거다.

posts.indexposts.*로 수정하자.

posts.*라고 적어주면, 라라벨 백엔드는 현재 요청된 라우트의 이름이 posts.으로 시작하는 모든 페이지를 전부 참(true)으로 판별한다.


:active="request()->routeIs(['posts.index', 'posts.edit'])" 이렇게 배열도 넘겨받을 수도 있다.

하지만 일반적인 CRUD 패턴(목록, 상세, 수정 등 전반)에서 메뉴 활성화를 유지하려면 복잡하게 코드를 늘릴 필요 없이 posts.* 와일드카드 방식을 적용하는 것이 소스코드 관리와 확장성 면에서 훨씬 유리하다.




PostController의 edit와 update 메서드


PostController.php의 edit 메서드

public function edit(Post $post)
{
    return view('posts.edit', [
        'post' => $post,
    ]);
}

인자 타입에 Post $post라고 적어주기만 하면, 주소창에 들어온 숫자 ID값(예: /posts/5/edit5)을 라라벨이 감지하여 데이터베이스에서 5번 게시글 레코드를 자동으로 조회(SELECT * FROM posts WHERE id = 5)한 뒤 객체 형태로 주입해 준다.

직접 무겁게 Post::find($id) 같은 코드를 짤 필요가 없다.

그렇게 찾아낸 글 데이터를 수정 폼 뷰 파일(posts.edit)로 안전하게 패스한다.





PostController.php의 update 메서드

public function update(Request $request, Post $post)
{
    $validated = $request->validate([
        'message' => 'required|string|max:300',
    ]);

    $post->update($validated);

    return redirect(route('posts.index'));
}

$post->update($validated);는 엘로퀀트 모델의 update 메서드를 통해 데이터베이스의 해당 행Row을 즉시 갱신(UPDATE posts SET message = ...)한다.

이때 데이터베이스의 updated_at 타임스탬프 시각이 현재 시간으로 업데이트되면서, 아까 index.blade.php에 추가했던 'edited' 문구가 화면에 표시된다.




나머지 코드들은 여기서 전부 설명했던 코드들이니 링크를 참고하자.




수정한 글에는 edited라는 텍스트가 표시되고 있다

글을 수정했더니, edited라는 텍스트가 표시되고 있다.




이로써 기본적인 글 수정의 큰 틀은 완성되었다.

하지만 지금 상태로는 다른 사람이 작성한 글도 마음대로 수정할 수 있는 심각한 문제가 남아있다.


그 문제에 대해서는 삭제 기능까지 구현한 다음에 해결해 보도록 하겠다.

이 글이 도움이 됐거나 유익했다면 스크롤을 조금만 더 내려서 댓글을 남겨주세요. (비로그인도 가능합니다!)
응원이나 피드백이 담긴 댓글은 제가 계속 블로그를 해나갈 수 있는 원동력이 됩니다. 😊

지인에게 보여주고 싶은 글이었다면 URL을 복사해서 메신저나 소셜 미디어에 공유해 주세요.
이전 포스트

댓글 쓰기

0 Comments

문의하기 양식