Livewire 2 → 3 버전업 후기

엽토군·2024년 3월 17일
0
post-thumbnail

서문

라라벨 9 + Livewire 2로 시작했던 개인 프로젝트가 하나 있는데, 이제 최신 Livewire는 3이고, 라라벨은 무려 11이 나왔으므로, 일단 Livewire라도 3으로 올려야겠다 싶어서 주말을 내서 올려봤다.

특별히 문제 없는 거 같아서 메모로 정리한다.

업그레이드 요령

기존 소스가 라라벨+Livewire 표준을 크게 벗어나지 않았다면, 업그레이드 가이드만 착실히 따라가도 웬만하면 별일 없다.

파일들은 폴더째로 이동시킨다.

mv app/Http/Livewire app/
mv resources/views/layouts resources/views/components/

네임스페이스와 뷰파일 경로는 '모두 찾아 바꾸기' 하면 된다.

  • namespace App\Http\Livewirenamespace App\Livewire
  • @include('layouts.~')@include('components.layouts.~')

이제 캐시를 내리고 composer.lock을 업데이트하면, 높은 확률로, 화면 자체는 일단 잘 뜬다.

php artisan view:clear
composer require livewire/livewire "^3.0"

하지만, 아마 군데군데 국소적으로 작동을 안/이상하게 하는 컴포넌트들이 있을 것이다.
이제 이걸 해결하는 게 진짜 본론.

특이사항

업그레이드 매뉴얼은 레거시를 그대로 가져가는 방안들을 이것저것 제시한다. config/livewire.php 파일에 무슨 설정을 넣어라, 무슨 태그를 뭐로 고쳐라...
근데 여기서는 그런 '레거시를 가져가는' 방향 대신, 현재 코드를 과감히 적응시키는 방향으로 가볼까 한다. 코드는 레거시로 취급하는 순간부터 정말로 그 생명력을 다하고 레거시로서의 존속만을 시작하기 때문이다. 사람도 그렇다지 않던가? 어떤 사람들은 스물다섯 살쯤에 죽어놓고는 일흔 넘기까지 무덤에 안 들어가기도 한다던데, 아직은 별로 헤비하지 않은 내 코드마저 산송장을 만들 필요는 없지 싶다.

여기서부터는

  1. 업그레이드 가이드 문서에 없었던 것
  2. 문서에 있었지만 이해/적용이 잘 안 됐던 내용
  3. 기타 언급할 가치가 있는 경험/사실

위주로만 기술한다.

Eloquent Model 자체를 property로 쓰면 안 됨

매뉴얼의 언급 자체는 진작 확인을 했는데, 읽기만 했을 때는 잘 이해가 안 됐다. 잘 작동하던 걸 왜 이제 와서 고치라고 하지? 모델 자체를 바인딩해 버리면 편리하지 않나?

이제 와서 내 기존 코드를 다시 보니 그 이유를 알 거 같았다.

public Note $note; // 이건 바깥에서 주입받는다 치고
public string $noteContent; // 이건 왜 따로 선언했더라?
public string $mode;

public function mount() {
	$this->noteContent = $this->note->content;
}
public function edit() {
	$this->note->save();
    $this->emitSelf('refresh');
}
<!-- 아... 수정하는 동안 수정 전 내용이 바뀌지 않게 하려고 그랬었구나... -->
<div x-html="$wire.noteContent"></div>
<div x-show="mode === 'edit'">
  <textarea wire:model="note.content"></textarea>
  <button wire:click="save">수정</button>
</div>

확실히 데이터 흐름이 이상하다. 실제 코드는 이것보다 훨씬 더 지저분했는데, 아마 그것도 이 때문일 것이다. 예컨대 이 div들 위의 div에는 아무짝에도 쓸모없는 x-data="{mode:$wire.mode}" 같은 것도 있었다.

물론 백엔드 입장에서는 public Note $note 하나만 들고 다니면 자기는 폼이 날 것이다. 하지만 프론트엔드는 그렇지가 않다. 프론트엔드에서 받고 주는 것은 $note 인스턴스가 아니고, 어디까지나 문자열, 객체, 불리언 등이기 때문이다. 요컨대, 클라이언트 측에 인스턴스를 통째로 주고 알아서 하라고 해 버리면, 프론트 쪽이 그만큼 버겁고 불안하고 비논리적인 구성이 되어 간다.

바인딩할 프로퍼티를 $noteContent로 바꾸고, 하는 김에 좀 더 고쳤다.

// 이 메소드만 이렇게 고침
public function edit() {
	$this->note->content = $this->noteContent;
	$this->note->save();
    $this->mode = 'show';
}
@if('mode' === 'show')
  <div>{!! nl2br($note->content) !!}</div>
@endif
@if('mode' === 'edit')
  <textarea wire:model="noteContent"></textarea>
  <button wire:click="save">수정</button>
@endif

확실히 아까보다 낫다. 백-프론트가 서로 책임을 비슷하게 나눠 지고 있고, $wire.foo 같은 것도 없어져서, 문제가 생겼을 때 제어 흐름 파악하기가 한결 수월하다. 프로젝트 현황에 따라서는 선뜻 적용하기 힘든 변경점일 수 있지만, 그렇다 해도 시간을 내서 대응할 가치가 있는 변경이다.

뷰에서의 프로퍼티 직접 변경 방법이 달라짐

사실 저 수정 버튼 옆에는 "취소" 버튼도 있다. 다시 '읽기 모드'로 돌아가는 기능인데, 원래는 이렇게 생겼었다.

<!-- alpine에 주로 의존하는 코드 -->
<button @click="$wire.mode = 'read'">수정 취소</button>

분명 이 코드는 Livewire 2에서는 별 문제가 없었는데, Livewire 3 매뉴얼에서는 set() 메소드를 쓰라는 소리를 하고, 그래서 써봤더니 브라우저 콘솔은 $wire.$wire.mode를 못 찾았다고 울고, 보통 성가신 게 아니었다.

Livewire 3에서의 정답은 다음과 같다.

<!-- alpine에 거의 의존하지 않는 코드 -->
<button wire:click="set('mode', 'read')">수정 취소</button>

'스냅샷이 없는 컴포넌트' 운운 오류가 뜬다면

어떤 컴포넌트의 뷰 일부는 이렇게 생겼었다.

@foreach($searchedItems as $item)
	@include('foo.bar', ['item' => $item])
@endforeach

아무 문제가 없을 거 같은 스니펫인데 문제가 있었다. $searchedItems가 일단 한 번이라도 변경되면, 그 다음부터는 이 구간이 완전히 먹통이 됐다.

브라우저 콘솔을 열어 보니 이런 오류가 보인다.

Uncaught Snapshot missing on Livewire component with id:

근데 이걸 구글에 넣고 검색하면 도움이 되는 정보가 하나도 나오지 않는다. 진작 종료된 Q&A 글 몇 개를 계속 뺑뺑이 돌면서 온갖 수정을 시도해 보다가, 좀 관계 없어 보이는 문서에서 이런 부분을 발견했다.

키는 필수 사항입니다

Vue나 Alpine 같은 프레임워크에서는 ... (중첩 루프 원소에 부여하는) 키는 의무가 아닙니다. ... 하지만 Livewire는 키에 좀더 많이 의존하고 있으며 키가 없으면 작동이 원활하지 않을 수 있습니다.

이게 정답이었다. Livewire 3부터는, foreach livewire 패턴을 쓸 거라면, 무조건 wire:key를 써야 한다. 그리고 사실 foo.bar 파일 안에는 @livewire 디렉티브가 있었기 때문에, 이 루프는 바로 이 문제에 걸리고 있는 것이었다.

@foreach($searchedItems as $item)
  <!-- 구태여 div를 하나 잡아준다. 이거면 해결이 된다. -->
  <div wire:key="searched-{{ $item->getKey() }}">
	@include('foo.bar', ['item' => $item])
  </div>
@endforeach

근데 왜 이게 Livewire 2에서는 문제가 안 되었을까? 문제가 아니었기 때문이다. 2.x 버전의 문서를 보면, wire:key는 필요할 때만 쓰면 되는 추가 기능으로써 대수롭지 않게 기술되어 있다. 이게 Livewire 3에 와서는 의무지만, 2에서는 선택이었던 것이다.

혹시 Livewire 2 기준으로 만들어둔 컴포넌트 뷰 안에 @foreach@for, @each가 있는가? 그 루프 안에서 livewire 컴포넌트가 그려지는지 확인해 보시라. 만약 그렇다면, 위와 같이 wire:key를 적용해야만 기능이 정상 작동할 것이다.

모델 바인딩은 기본 지연됨

개발한 컴포넌트 중에는 목록 컴포넌트가 하나 있는데, '내 것만 보기' 체크박스를 붙인 것이었다. 짐작하시다시피 boolean 모델을 주고받으면 되는 필터 입력이다. 그리고 목록 페이지라는 건 아무래도 조회 조건을 URL 쿼리변수로 주고받는 편이 여러모로 편리하다.
그래서 체크박스 + 쿼리변수 조합으로 이 목록 컴포넌트를 개발했다.

public $mineOnly = false;
private function search() {
	return $query->where(...)
    	->when($this->mineOnly, fn($q) => $q->mine())
		->get();
}
public function render() { $this->search(); return view(...); }
<input type="checkbox" wire:model="mineOnly" />

이상의 소스는 Livewire 2에서는 잘 돌았다. 체크박스를 클릭하면 정말로 내 것만 목록에 떴고, 다시 클릭해서 선택을 해제하면 내 것이 아닌 자료도 떴다. 그런데 이 코드가 3에서는 고장이 났다. 아무리 체크박스를 체크하고 해제하고 해도 항상 모든 자료가 목록에 떴다.

뭐지? 했다가 앗차! 했다. 일단 하나를 빼먹었다. Livewire 3의 가장 큰 변경 중 하나, 모델 바인딩 기본 행동이 live에서 defer로 바뀐 것이다.

수정 자체는 간단했다. live만 적용하면 된다.

<input type="checkbox" wire:model.live="mineOnly" />

이 필터에는 '검색버튼' 같은 것이 따로 없었고, 이 체크박스는 클릭 즉시 필터에 적용이 돼야 한다. 그러므로 debounce, blur 등의 수정자(modifier)는 필요없다.

Laravel 10 미만에서는 일부 기능이 제한됨

그렇게 고치니까 체크박스 필터 자체는 작동하는 거 같긴 한데, 정작 URL 쿼리변수는 바뀌지 않는다. ?mineOnly=false 같은 게 주소창에서 수시로 바뀌어야 할 거 같은데 그러지 않는다.

뭐지? 했다가 앗차! 했다. 또 하나를 빼먹었다. Livewire 3의 또다른 큰 변경 하나, 이런저런 프로퍼티 옵션들을 속성(Attribute)으로 주게 된 것이다.

원래대로라면, 이 고장은 매뉴얼에 있는 기본 용법만 적용하면 바로 고쳐져야 한다.

use Livewire\Attributes\Url;

#[Url]
public boolean $mineOnly = false;

그런데 안 된다. 여전히 mineOnly 프로퍼티는 Livewire 내부 데이터로만 교환될 뿐 주소창에 뜨지 않는다. 뭐지? 문서가 틀렸나? (결론부터 말하면 틀렸다기보다는 자세한 언급이 없다. 잠시 후 다시 설명함)
매뉴얼을 허둥지둥 뒤지다가 맨 끝에 이런 언급을 본다.

쿼리 문자열을 컴포넌트 메소드로 정의하실 수도 있습니다. 프로퍼티를 동적으로 다뤄야 할 때 편리합니다.

'그래? 동적이니 조건부니 그런 건 모르겠고 난 그냥 이 기능이 작동을 좀 했으면 좋겠어.' 하고, 이것으로 대신 적용을 해본다.

public boolean $mineOnly = false;

protected function queryString() {
	return ['mineOnly'];
}

그랬더니 작동이 된다. 이제 다른 비슷한 목록 컴포넌트도 적용을 하려고 열어 보니, 오히려 수정을 안 한 컴포넌트들이 잘 작동하고 있었다. Livewire 2 기준 형식 그대로 어디 고친 게 없는데도 불구하고.

// 원래 이 코드였다. 괜히 고쳤나...?
public boolean $mineOnly = false;
protected $queryString = ['mineOnly'];

요컨대 3.x 문서가 약속하는 #[Url] Attribute는 효험이 없었다는 이야기다. 왜 이렇지? 하고 살펴보다 보니 문서에는 써 있는 except 파라미터가 지금 내 프로젝트에서는 가용하지 않다는 것도 알게 되었다. 뭐야? 설마? 하고 버전을 확인해 보니... 지금 설치된 Livewire 3의 버전이 매우 낮다.

$ composer show livewire/livewire
versions : * v3.0.0-beta.6

이건 또 왜 이래? 하고 생각해 보니, 아무래도 이 프로젝트의 라라벨 버전이 최신이 아니라서, 가장 최신의 Livewire 3를 설치할 수 없었던 것 같다.

$ php artisan --version
Laravel Framework 9.52.16

except 파라미터 자체는 작년 12월경에나 등장했다. 포함된 릴리즈는 Livewire 3.2.0이다. 매뉴얼은 이러한 역사에는 아무 관심 없이 그냥 최신 "3.x" 기준으로 모든 것을 설명하고 있다. 그것 자체는 그럴 수 있다. 하지만 이 패키지는 Laravel 패키지잖아? 그렇다면 Laravel 버전을 타는 부분들은 좀 언급을 해줬어도 좋지 않았을까? 최소한 업그레이드 문서에서는 라라벨 버전 관련 언급이 좀 있었어야 하는 게 아닐까?

쿼리변수는 기본값일 때 생략된다

어쨌든 이제는 체크박스+쿼리변수 기능이 내가 수용 가능한 수준까지는 동작을 하는데, 한 가지가 달라졌다. 해당 프로퍼티에 제공된 값이 기본값과 같으면, URL 쿼리변수에서 생략된다.

  • 기존: 사용자가 체크를 해제하면 mineOnly=false가 URL에 찍힘.
  • 현재: 사용자가 체크를 해제해도 mineOnly=false가 URL에 안 찍힘.

물론 이게 더 좋긴 한데, 혹시 기존 동작을 그대로 가져갈 수는 없나? 이에 관해서는 의외로 업그레이드 가이드 한쪽 구석에 방법이 적혀 있었다. (그리고 분위기를 보아하니, 이걸 싫어한 사람들이 많았던 모양이다. 이제는 기본 동작이 된 그 효과를 얻기 위해, 예전에는 추가 삽질이 필요했던 모양이다.)

public function queryString() {
	return ['incomplete' => ['keep' => true]];
}

후기

결과적으로 현재 상태는, 전반적으로는 별 문제 없이 온보딩했는데, 라라벨 버전이 낮다는 이유로 Livewire 3의 모든 기능에 접근하지 못하는 상태다.

아무래도 조만간에는 라라벨 9 → 10 버전업 후기를 쓰게 될 모양이다.

profile
5년차 PHP 개발자입니다.

0개의 댓글