Skip to content

모핑

Livewire 컴포넌트가 브라우저의 DOM을 업데이트할 때, "morphing"이라고 부르는 지능적인 방식을 사용합니다. _morph_라는 용어는 _replace_와 같은 단어와 대조됩니다.

컴포넌트가 업데이트될 때마다 컴포넌트의 HTML을 새로 렌더링된 HTML로 _교체_하는 대신, Livewire는 현재 HTML과 새로운 HTML을 동적으로 비교하여 차이점을 식별하고, 변경이 필요한 부분에만 외과적으로 HTML을 수정합니다.

이 방식의 장점은 컴포넌트에서 변경되지 않은 기존 요소들을 보존할 수 있다는 것입니다. 예를 들어, 이벤트 리스너, 포커스 상태, 폼 입력 값 등이 Livewire 업데이트 사이에 모두 보존됩니다. 물론, morphing은 매번 새로운 DOM을 지우고 다시 렌더링하는 것에 비해 성능도 향상됩니다.

Morphing의 작동 방식

Livewire가 Livewire 요청 사이에 어떤 요소를 업데이트할지 결정하는 방식을 이해하려면, 이 간단한 Todos 컴포넌트를 살펴보세요:

php
class Todos extends Component
{
    public $todo = '';

    public $todos = [
        'first',
        'second',
    ];

    public function add()
    {
        $this->todos[] = $this->todo;
    }
}
blade
<form wire:submit="add">
    <ul>
        @foreach ($todos as $item)
            <li>{{ $item }}</li>
        @endforeach
    </ul>

    <input wire:model="todo">
</form>

이 컴포넌트의 초기 렌더링은 다음과 같은 HTML을 출력합니다:

html
<form wire:submit="add">
    <ul>
        <li>first</li>

        <li>second</li>
    </ul>

    <input wire:model="todo">
</form>

이제 입력 필드에 "third"를 입력하고 [Enter] 키를 눌렀다고 가정해봅시다. 새로 렌더링된 HTML은 다음과 같습니다:

html
<form wire:submit="add">
    <ul>
        <li>first</li>

        <li>second</li>

        <li>third</li> 
    </ul>

    <input wire:model="todo">
</form>

Livewire가 컴포넌트 업데이트를 처리할 때, 원래의 DOM을 새로 렌더링된 HTML로 _morph_합니다. 다음 시각화는 그 작동 방식을 직관적으로 이해할 수 있게 해줍니다:

보시다시피, Livewire는 두 HTML 트리를 동시에 순회합니다. 각 트리에서 요소를 만날 때마다, 변경, 추가, 제거 여부를 비교합니다. 변경 사항이 감지되면, 해당 부분만 외과적으로 적절하게 수정합니다.

Morphing의 한계

다음은 morphing 알고리즘이 HTML 트리의 변경을 올바르게 식별하지 못해 애플리케이션에 문제를 일으키는 시나리오입니다.

중간 요소 삽입

가상의 CreatePost 컴포넌트에 대한 다음 Livewire Blade 템플릿을 살펴보세요:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    @if ($errors->has('title'))
        <div>{{ $errors->first('title') }}</div>
    @endif

    <div>
        <button>Save</button>
    </div>
</form>

사용자가 폼을 제출하려고 시도했지만, 유효성 검사 오류가 발생한 경우 다음과 같은 문제가 발생합니다:

보시다시피, Livewire가 오류 메시지용 새로운 <div>를 만났을 때, 기존 <div>를 제자리에 변경해야 할지, 아니면 중간에 새로운 <div>를 삽입해야 할지 알지 못합니다.

좀 더 명확하게 무슨 일이 일어나는지 다시 설명하자면:

  • Livewire는 두 트리에서 첫 번째 <div>를 만납니다. 동일하므로 계속 진행합니다.
  • Livewire는 두 트리에서 두 번째 <div>를 만나고, 동일한 <div>라고 생각합니다. 단지 내용만 바뀌었다고 판단합니다. 그래서 오류 메시지를 새로운 요소로 삽입하는 대신, <button>을 오류 메시지로 변경합니다.
  • Livewire는 이전 요소를 잘못 수정한 후, 비교의 끝에서 추가 요소가 있음을 인지합니다. 그런 다음 해당 요소를 새로 생성하여 이전 요소 뒤에 추가합니다.
  • 따라서, 단순히 이동되어야 할 요소를 파괴했다가 다시 생성하게 됩니다.

이 시나리오는 거의 모든 morph 관련 버그의 근본 원인입니다.

이러한 버그가 초래하는 구체적인 문제점은 다음과 같습니다:

  • 업데이트 사이에 이벤트 리스너와 요소 상태가 손실됨
  • 이벤트 리스너와 상태가 잘못된 요소에 할당됨
  • 전체 Livewire 컴포넌트가 리셋되거나 중복될 수 있음 (Livewire 컴포넌트도 DOM 트리의 단순한 요소이기 때문)
  • Alpine 컴포넌트와 상태가 손실되거나 잘못 배치될 수 있음

다행히도, Livewire는 다음과 같은 접근 방식을 통해 이러한 문제를 완화하기 위해 노력해왔습니다:

내부 look-ahead

Livewire는 morphing 알고리즘에 추가 단계로, 요소를 변경하기 전에 이후 요소와 그 내용을 확인합니다.

이로 인해 위와 같은 시나리오가 많은 경우에서 발생하지 않게 됩니다.

다음은 "look-ahead" 알고리즘이 동작하는 시각화입니다:

Morph 마커 주입

백엔드에서 Livewire는 Blade 템플릿 내부의 조건문을 자동으로 감지하여, Livewire의 JavaScript가 morphing 시 가이드로 사용할 수 있는 HTML 주석 마커로 감쌉니다.

다음은 이전 Blade 템플릿에 Livewire가 주입한 마커가 추가된 예시입니다:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    <!--[if BLOCK]><![endif]-->
    @if ($errors->has('title'))
        <div>Error: {{ $errors->first('title') }}</div>
    @endif
    <!--[if ENDBLOCK]><![endif]-->

    <div>
        <button>Save</button>
    </div>
</form>

이렇게 마커가 템플릿에 주입되면, Livewire는 변경과 추가의 차이를 더 쉽게 감지할 수 있습니다.

이 기능은 Livewire 애플리케이션에 매우 유용하지만, 정규식을 통해 템플릿을 파싱해야 하므로 때때로 조건문을 제대로 감지하지 못할 수 있습니다. 이 기능이 애플리케이션에 도움이 되기보다는 방해가 된다면, 애플리케이션의 config/livewire.php 파일에서 다음 설정으로 비활성화할 수 있습니다:

php
'inject_morph_markers' => false,

조건문 감싸기

위의 두 가지 해결책으로도 상황이 해결되지 않는다면, morphing 문제를 가장 확실하게 피하는 방법은 조건문과 반복문을 항상 존재하는 자체 요소로 감싸는 것입니다.

예를 들어, 위의 Blade 템플릿을 감싸는 <div> 요소로 다시 작성하면 다음과 같습니다:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    <div> 
        @if ($errors->has('title'))
            <div>{{ $errors->first('title') }}</div>
        @endif
    </div> 

    <div>
        <button>Save</button>
    </div>
</form>

이제 조건문이 항상 존재하는 요소로 감싸졌으므로, Livewire는 두 HTML 트리를 올바르게 morphing할 수 있습니다.

Morphing 우회

특정 요소에 대해 morphing을 완전히 우회해야 하는 경우, wire:replace를 사용하여 Livewire가 기존 요소를 morphing하려고 시도하는 대신 해당 요소의 모든 자식을 교체하도록 지시할 수 있습니다.

나를 위한 문서 한글화