본문으로 건너뛰기

TipTap 에디터와 이미지 업로드

글 작성/수정 화면의 본문 에디터는 TipTap을 사용합니다.

담당 파일은 다음입니다.

resources/js/editor.js

왜 textarea가 따로 있을까?

글쓰기 화면에는 두 개의 본문 영역이 있습니다.

<div id="editor"></div>
<textarea name="content" id="content" class="hidden"></textarea>

사용자가 직접 보는 것은 div#editor입니다.
하지만 HTML form이 서버로 전송하는 값은 name="content"가 붙은 textarea입니다.

그래서 TipTap 내용이 바뀔 때마다 textarea에 HTML을 복사합니다.

onUpdate({ editor }) {
if (contentInput) {
contentInput.value = editor.getHTML()
}
}

저장 흐름

사용자가 TipTap에 글 작성
-> editor.getHTML()
-> hidden textarea value에 저장
-> 저장 버튼 클릭
-> POST /admin/posts
-> posts.content 컬럼에 저장

수정 화면에서 기존 글 불러오기

수정 화면에서는 기존 본문이 textarea에 들어 있습니다.

<textarea name="content" id="content" class="hidden">
{{ old('content', $post->content) }}
</textarea>

TipTap은 초기화될 때 이 값을 읽습니다.

content: contentInput ? contentInput.value : '',

그래서 수정 화면에 들어가면 기존 글 내용이 에디터에 보입니다.

에디터 확장 기능

사용하는 TipTap 확장은 다음과 같습니다.

확장역할
StarterKit기본 텍스트 편집 기능
Image이미지 삽입
Link링크 삽입
CodeBlockLowlight코드블록 하이라이팅
lowlight코드 하이라이팅 엔진
highlight.js언어별 문법 규칙

지원 언어는 현재 JavaScript, CSS, PHP, HTML입니다.

툴바 버튼

글쓰기 화면의 버튼은 JS에서 id로 찾아 이벤트를 붙입니다.

document.getElementById('btn-bold')?.addEventListener('click', () => {
editor.chain().focus().toggleBold().run()
})

?.는 요소가 없으면 에러를 내지 않고 넘어가는 문법입니다.
덕분에 에디터가 없는 페이지에서 이 JS가 로드되어도 깨지지 않습니다.

이미지 업로드 흐름

이미지 버튼을 누르면 파일 선택창을 만들고, 선택된 파일을 서버로 보냅니다.

이미지 버튼 클릭
-> input type=file 생성
-> 파일 선택
-> FormData에 image 추가
-> POST /admin/upload-image
-> 서버가 URL 반환
-> TipTap에 이미지 삽입

CSRF 토큰

Laravel은 POST 요청에 CSRF 토큰이 필요합니다.

레이아웃에 토큰이 들어 있습니다.

<meta name="csrf-token" content="{{ csrf_token() }}">

JS에서는 이 값을 읽어서 헤더에 넣습니다.

const token = document.querySelector('meta[name="csrf-token"]').content

서버 이미지 처리

서버는 ImageController@upload가 처리합니다.

$request->validate([
'image' => 'required|image|max:2048'
]);

이미지 파일만 허용하고 최대 2MB까지 허용합니다.

$path = $request->file('image')->store('images', 'public');
$url = '/storage/' . $path;

파일은 storage/app/public/images에 저장되고, 브라우저에서는 /storage/images/...로 접근합니다.

붙여넣기 이미지

클립보드에 이미지가 있으면 paste 이벤트에서 감지합니다.

editor.view.dom.addEventListener('paste', async (event) => {
const items = event.clipboardData?.items
})

이미지 타입이면 기본 붙여넣기를 막고 서버에 업로드합니다.

event.preventDefault()

이렇게 하지 않으면 이미지가 base64 문자열로 본문에 그대로 박힐 수 있습니다.

storage:link

업로드 이미지를 웹에서 보려면 심볼릭 링크가 필요합니다.

php artisan storage:link

Railway 배포 명령에도 이 작업이 포함되어 있습니다.