Toast UI Editor는 NHN에서 개발한 Markdown 기반의 WYSIWYG 에디터입니다.
Toast UI Editor는 다음과 같은 특징이 있습니다.
- Markdown과 WYSIWYG 모드 실시간 전환
- 이미지 업로드 및 관리
- 표, 차트 삽입 기능
- 문법 강조(Syntax Highlighting)
- 자동 링크
- 국제화(i18n) 지원
바로 사용해보겠습니다.
다음 명령어를 사용해 Toast UI Editor를 npm install 합니다.
npm install @toast-ui/react-editor --legacy-deps-peer
여기서 --legacy-deps-peer를 사용하는 이유는 React 버전 충돌을 방지하기 위해서입니다. Toast UI Editor는 공식적으로 React 16.x ~ 17.x 버전을 지원하지만, 현재 많은 프로젝트들은 React 18.x 버전을 사용하고 있습니다.
😫 만약 --legacy-deps-peer 옵션 없이 설치하면 다음과 같은 에러가 발생할 수 있습니다:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! peer react@"^16.x || ^17.x" from @toast-ui/react-editor@3.2.3
이는 패키지의 peer dependency 요구사항과 실제 프로젝트 환경이 일치하지 않아서 발생하는 문제입니다. --legacy-deps-peer 옵션을 사용하면 이러한 버전 불일치를 무시하고 설치를 진행할 수 있으며, 실제로 React 18.x 환경에서도 대부분의 기능이 정상적으로 작동합니다.
그 다음 커스텀 Toast UI Editor를 제작하기 위해 다음과 같이 컴포넌트를 세팅합니다.
import { Editor } from '@toast-ui/react-editor'; // Toast UI Editor의 React 래퍼 컴포넌트
export default function ToastEditor({ onChange, handleImage }) {
const editorRef = useRef<Editor>(null);
return (
<Editor
ref={editorRef}
initialEditType="wysiwyg" // 기본 모드를 위지윅으로 지정
useCommandShortcut // 키보드 단축키 사용 활성화
hooks={{ addImageBlobHook: handleImage }} // 이미지 처리 함수
/>
);
}
여기서 editorRef를 사용한 이유는 Toast UI Editor의 인스턴스에 직접 접근하여 에디터의 메서드와 이벤트를 제어하기 위해서입니다. editorRef는 다음과 같은 역할을 수행합니다.
- 에디터 인스턴스의 지속성 보장
- 컴포넌트가 리렌더링 되어도 에디터 인스턴스 유지
- 불필요한 재생성을 방지하여 성능 최적화
- 에디터의 메서드 접근
getInstance()를 통해 에디터의 핵심 기능에 접근getHTML(),getMarkdown(),setHeight()등의 메서드 사용 가능- 이벤트 핸들링
- 컨텐츠 변경, 커서 이동, 포커스 변경 등의 이벤트 감지
그리고 참조 시점을 설정하기 위해 다음과 같은 useEffect훅과 이벤트 리스너를 작성합니다.
useEffect(() => {
const instance = editorRef.current?.getInstance();
if (instance) {
// change 이벤트 리스너 등록
instance.on('change', () => {
// 변경된 내용을 HTML 형식으로 가져옴
const content = instance.getHTML();
// 상위 컴포넌트로 변경된 내용을 전달
onChange?.(content);
});
}
}, [onChange]);
이 useEffect훅은 다음과 같은 동작을 수행합니다.
- 컴포넌트가 마운트되고 onChange prop이 변경될 때마다 리스너를 재설정
editorRef.current?.getInstance()를 통해 실제 Toast UI Editor 인스턴스를 가져옴instance.on('change', ()=>...)을 통해 사용자가 텍스트를 입력, 삭제, 서식 변경등의 작업을 할 때마다 이벤트 리스너 실행- 콜백처리
instance.getHTML(): 현재 에디터의 내용을 HTML 문자열로 가져옴onChange?.(content): 옵셔널 체이닝을 사용해 상위 컴포넌트에 변경된 내용을 전달
먼저 텍스트 에디터를 사용할 페이지에서 작성한 TextEditor 컴포넌트를 Next.js의 dynamic import를 사용하여 동적으로 불러옵니다.
const ToastEditor = dynamic(() => import('../../../../../_components/ToastEditor'), {
ssr: false, // 서버사이드 렌더링 비활성화
});
이렇게 ToastEditor를 동적으로 import 한 이유는 다음과 같습니다.
- SSR 호환성
- Toast UI Editor는 브라우저의 window 객체에 의존성이 있어 SSR 환경에서 직접 렌더링 할 경우 오류가 발생. 그러므로
ssr: false를 추가하여 클라이언트 사이드에서만 렌더링이 되도록 설정- 번들 사이즈 최적화
- Toast UI Editor는 아주 무거운 라이브러리이므로 dynamic import를 통해 에디터가 실제로 필요한 시점에만 로드되도록 지연 로딩(lazy loading) 구현
이러한 방식으로 import 함으로써 초기 로딩 속도를 개선하고, SSR 관련 오류를 방지합니다.
그 뒤에 렌더링 부분에서 <ToastEditor />에 value, onChange 등을 작성하여 간편하게 사용할 수 있습니다.
저희 프로젝트에서는 사용자가 에디터에 이미지를 업로드 하는 순간, 이미지 파일을 백엔드로 전송하고, 백엔드에서 이미지의 url을 반환해 그 주소를 에디터에 <img />태그를 활용해 입력하는 방식을 사용합니다.
이를 구현하기 위해 에디터의 이미지 처리를 위한 함수를 작성합니다.
먼저 ToastEdiotr.tsx를 다음과 같이 작성합니다.
interface ToastEditorProps {
...
handleImage: (blob: File, callback: (url: string) => void) => Promise<void>;
}
export default function ToastEditor({ ..., handleImage}: ToastEditorProps){
...
return (
<Editor
ref={editorRef}
initialValue={initialValue}
hooks={{ addImageBlobHook: handleImage }}
/>
);
}
다음과 같이 인터페이스를 작성한 이유는 이미지 업로드 처리를 위한 명확한 타입 정의와 콜백 패턴을 구현하기 위해서입니다.
handleImage함수의 매개변수:
blob: File: 사용자가 업로드한 이미지 파일을 File 객체로 받음callback: (url: string) => void: 이미지 업로드 완료 후 URL을 에디터에 반영하기 위한 콜백 함수Promise<void>: 비동기 처리를 위한 Promise 반환
그 다음 구현 부분의 hooks={{ addImageBlobHook: handleImage }}는 다음과 같은 특징이 있습니다.
- Toast UI Editor의 이미지 업로드 훅을 사용하여 이미지 처리 로직을 연결
- 에디터에 이미지가 추가될 때마다 handleImage 함수가 자동으로 호출됨
addImageBlobHook은 사용자가 드래그 앤 드롭, 업로드 버튼, 붙여넣기 등으로 이미지를 추가할 때마다 자동으로 호출하는 이미지 처리 훅입니다.그 다음 실제 구현 페이지에서 다음과 같이 이미지 처리 함수를 작성합니다.
const handleImage = useCallback(async (file: File, callback: (url: string) => void) => {
const imageUrl = await postPic(file);
callback(imageUrl);
}, []);
...
return (
<ToastEditor
initialValue={content}
onChange={(value: string) => setContent(value)}
handleImage={handleImage}
/>
)
handleImage함수는 다음과 같이 동작합니다.
useCallback사용
- 불필요한 리렌더링 방지를 위해 메모이제이션된 콜백 함수 생성
- 비동기 처리
async/await을 사용해 이미지 업로드 작업을 비동기적으로 처리postPic(file)으로 이미지 파일을 서버에 업로드하고 URL을 반환 받음- 콜백 실행
- 서버로부터 받은 imageUrl을
callback함수에 전달- 에디터는 이 URL을 사용해 이미지를 본문에 삽입
이렇게 구현된 handleImage함수를 ToastEditor 컴포넌트의 props로 전달하여 이미지 업로드 기능을 완성합니다.
Toast UI Editor는 추가적인 커스터 마이징이 가능하며, 위와 같은 설정을 따르면 간단하게 텍스트 에디터를 구현할 수 있습니다.