[React] TOAST UI Editor 사진 첨부 문제

jinyoung·2022년 12월 18일
2
post-thumbnail

Why TOAST UI Editor?

엘리스 2차 프로젝트를 진행 중이다. 일종의 커뮤니티를 주제로 기획과 구조 설계가 끝나고 본격적인 구현에 들어간 상태이다. 이번에 게시물 생성 페이지를 맡았고, 게시물 작성 시 마크다운 문법을 지원하기로 결정하였다.

관련 라이브러리들은 몇가지 있었다. 가장 먼저 찾은 것은 @uiw/react-md-editor였는데 UI 디자인이 뭔가 구렸다. 또한 npm trends 검색 결과 최근 업데이트나 사용량이 영 좋지 못해서 다른 것을 찾아보았다.

이번에는 한글로 마크다운 에디터를 검색하니 TOAST UI Editor에 대한 게시물이 많은것을 알 수 있었다. 평소에도 TOAST 웹 사이트에서 포스팅하는 게시물들을 보았기 때문에 호감이갔다. 추가적으로 프로젝트의 레퍼런스 사이트 중 하나가 프로그래머스 커뮤니티였는데, 해당 커뮤니티 게시물 작성 페이지에서도 TOAST UI Editor를 사용하는 것을 확인할 수 있었다. 직접 사용해보니 디자인도 깔끔하고 반응형 웹 사이즈에도 잘 대응했다.

그렇게 TOAST UI Editor를 사용하게 되었다.

문제 발생

이 라이브러리에서 제공하는 에디터 컴포넌트는 사용하기에 굉장히 간단했다. 그래서 금방 구현할 수 있었고, 개발 환경 웹 브라우저에서 테스트를 해보았다. 에디터 내부의 텍스트 핸들링, 읽기 등 다른 기능들은 모두 잘 되었다. 하지만 사진을 첨부할 때 큰 문제가 발생했다.

GIF 캡쳐를 보자.

이처럼 사진을 첨부할 경우 사진의 URL이 base64로 인코딩된 아주 긴 문자열로 변환된다. 이런식으로 이미지 링크가 걸리면 사용자가 글 작성하는데에 문제가 있다. 그래서 벨로그 에디터나 깃헙 사진 첨부와 같이 적당한 이미지 URL로 바꿔주어야한다.

작성 중인 이 글 위의 GIF 이미지의 URL은 https://velog.velcdn.com/images/matajeu/post/0cedfaf3-d108-412f-b58a-39baba3e210f/image.gif 으로 표시되어있다.

해결 과정

해결 방향 찾기

벨로그나 깃헙에서 첨부한 이미지는 url이 지정된다. 벨로그에서 해당 url을 살펴보면
https://velog.velcdn.com/images/{유저 아이디}/post/{랜덤문자열}/image{파일형식}
와 같다.

즉, 첨부하는 순간 해당 이미지를 서버에 저장한 뒤 저장된 이미지의 주소를 출력해주는 것이다. 그렇다면 이미지를 첨부할 때 발생하는 이벤트를 감지해야 이 문제를 해결할 수 있을 것이라고 생각했다.

TOAST UI Editor에서는 이미지가 첨부될 때 addImageBlobHook이라는 훅이 실행된다.
이 이벤트를 가로채는 것이 문제 해결의 키포인트이다

이미지 첨부 이벤트 막기

공식 문서에서 이 기본 hook을 제거하는 방법이 removeHook임을 알 수 있다.

해당 메서드를 사용하기 위해서 에디터의 DOM 객체를 다음과 같이 useRef를 이용하여 핸들링해야한다.

const editorRef = useRef<Editor>(null); // 핸들링
<Editor
	ref={editorRef}
	placeholder="이 입력폼은 마크다운 문법을 지원합니다."
/>

그러면 editorRef.current.getInstance().removeHook('addImageBlobHook')을 실행시켜주면 이미지 첨부 이벤트를 막을 수 있다.

이미지 첨부 커스텀 이벤트 등록하기

이제 커스터마이징한 Hook을 추가해주려면 반대로 addHook()을 사용해서 구현할 수 있다. hook의 이름은 이전에 제거한 이벤트와 동일하게 addImageBlobHook로 지정한다.

editorRef.current.getInstance().addHook('addImageBlobHook', async (blob, callback) => {
	...
})

이때 콜백 함수의 인자로 blob과 callback을 받는다. blob은 첨부되는 이미지 파일 데이터이며, callback 함수는 에디터에 이미지 마크다운 텍스트를 입력해주는 함수이다.

기본 동작은 이 blob을 이미지 링크 부분에 base64 인코딩된 문자열이 들어오며 다시 callback() 함수의 첫번째 인자로 들어가서 아주 긴 문자열의 이미지 링크로 삽입된다. 즉, callback() 함수의 인자로 들어가는 url을 우리가 원하는 url로 입력되도록 해야한다.

DB에 저장된 이미지 링크를 불러오려면 서버단과 API 통신이 필요하다. 서버쪽 담당자와 이 이슈를 토론한 결과 FormData 타입에 blob 데이터를 넣어서 보내달라고 하였다. 그래서 다음과 같이 API 통신을 구현했다.

editorRef.current
         .getInstance()
         .addHook('addImageBlobHook', async (blob: File, callback: HookCallback) => {
         	// blob 자체가 file 임,
            const formData = new FormData();
            // 아래와 같이 저장하면 formData {image:blob} 형태가 됨
            formData.append('image', blob);
            // 서버에 이미지 저장 및 저장된 이미지 url 응답 받기
            const url = await axios.post('/api/...', formData);
            // 에디터에 url과 파일 이름을 이용한 마크다운 이미지 문법 작성 콜백 함수
            callback('...' + url, blob.name);
            return false;
         });

이때 blob과 callback의 type은 @toast-ui/react-editor의 라이브러리 폴더를 확인해서 알 수 있었다.

type HookCallback = (url: string, text?: string) => void;

...

export type HookMap = {
  addImageBlobHook?: (blob: Blob | File, callback: HookCallback) => void;
};

따라서 다음과 같이 blob의 타입은 Blob | File, callback의 경우 HookCallback으로 지정되어있다. 그런데 blob의 경우 사용자 로컬에 저장된 이미지 파일 이름을 받아와야하기 때문에 File으로만 지정해주었다. Blob에는 name 메서드가 없기 때문이다.

결과

API 연동 테스트 성공 후 다음과 같이 이미지 첨부 이벤트가 동작되었다.

후기

TOAST UI Editor는 잘 만들어진 라이브러리라고 생각한다. 디자인도 깔끔하고 사용하기에도 정말 편하다. 하지만 아쉬운 부분도 존재한다.

그 중 가장 불편한 부분이 React 버전 호환성 문제이다. 우리팀이 사용하는 리액트는 18.2.0 버전을 사용하고 있는데, 이 버전과 호환이 되지 않는다. 그래서 노드 패키지 설치할 때 패키지 종속성 에러가 발생해서 --force 옵션을 넣어서 설치된다. 그리고 이후 추가적으로 패키지 설치할 때마다 같은 문제가 또 발생한다.

이 라이브러리를 이용하여 화면단이 어느정도 완성되었는데, 지금은 리액트 버전을 낮추거나 다른 라이브러리를 찾아야하는 상황이라서 곤란한 상황이다. ^^;

profile
개발 회고록

2개의 댓글

comment-user-thumbnail
2024년 5월 18일

많은 도움 되었습니다!

1개의 답글