
개발환경
next: v14.2.15
@toast-ui/react-editor: v:3.2.3
1. 📙 TUI Editor 설치
<bash />npm i @toast-ui/react-editor

설치를 하자마자 에러가 발생해 머리를 잡고 싶었지만 ,,
이는 이슈에도 나와있듯이 react 18을 지원하고 있지 않아서 버전 오류가 발생하는 것이다.
이때 --force
옵션을 사용하여 설치하면 설치가 완료된 모습을 볼 수 있다.
<bash />npm i @toast-ui/react-editor --force

--force 옵션을 사용하여 설치하는 것은 무슨 의미일까?
터미널에서도 볼 수 있듯이 의존성 충돌이나 경고를 무시하고 설치를 강제로 진행한다.
최후의 수단으로 사용하는 옵션이기 때문에 이를 염두해 두고 사용하여야 한다.
추가사항
React 환경에서 @toast-ui/react-editor 가 아닌 @toast-ui/editor를 설치해서 사용할 경우, 패키지를 읽어오지 못하는 오류가 발생할 수 있다. 이는 깃헙 이슈에 있는 방법으로 해결할 수 있지만, react-editor를 설치하면 이슈에 나온 방법처럼 추가 설정을 해줄 필요가 없다.
또한 innerHTML을 찾을 수 없다는 오류도 발생하기 때문에 React를 사용할 때는 꼭 react-editor 로 패키지를 다운로드 받아주자.
2. 📙 프로젝트에 적용하기
<typescript />
import "@toast-ui/editor/toastui-editor.css";
import { Editor } from "@toast-ui/react-editor";
export default function TuiEditor() {
return (
<>
<Editor
height="24rem"
initialValue=" "
initialEditType="wysiwyg"
hideModeSwitch={true}
/>
</>
);
}
툴바나 추가적인 설정들은 한 컴포넌트에서 관리하고 싶어 따로 TuiEditor 컴포넌트를 만들어 분리시켜줬다.
props로는 높이랑 초기 값, 에디터 편집 타입과 markdown, wysiwyg 모드 변환 스위치에 대한 설정만 해주었다.

이외에도 많은 옵션들은 여기에서 확인해볼 수 있다.

제대로 설정이 되었다면 위와 같은 결과 화면을 띄울 수 있다 ✨
3. 📙 서버에 데이터 전송하기
이제 입력한 값을 서버에 전송해보도록 하자
<typescript />
import "@toast-ui/editor/toastui-editor.css";
import { Editor } from "@toast-ui/react-editor";
interface Props {
editorRef: React.RefObject<Editor>;
}
export default function TuiEditor({ editorRef }: Props) {
return (
<>
{editorRef && (
<Editor
ref={editorRef}
height="24rem"
initialValue=" "
initialEditType="wysiwyg"
hideModeSwitch={true}
/>
)}
</>
);
<typescript />
export default function PostForm() {
const { mutate } = useCreatePost();
const editorRef = useRef<Editor>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const contents = editorRef.current?.getInstance().getHTML(); // HTML로 값 가져오기
mutate({ contents }); // 데이터 서버 전송
};
return(
<form onSubmit={handleSubmit}>
<TuiEditor editorRef={editorRef}/>
<Button
ariaLabel="게시글 작성 완료 버튼"
type="submit"
>
작성하기
</Button>
</form>
);
}
Editor의 값은 ref를 생성해서 연결시킨 후 가져올 수 있다.
<typescript />
const contents = editorRef.current?.getInstance().getHTML();
현재 코드는 입력한 콘텐츠를 HTML로 변환하여 가져온 것이다.
값을 가져오는 방법에는 두 가지가 존재한다.
getHTML()
- 입력된 콘텐츠를 HTML 문자열로 반환한다.
getMarkdown()
- 입력된 콘텐츠를 Markdown 문자열로 반환한다.


이처럼 getHTML()
은 HTML로 변환된 문자열을, getMarkdown()
은 Markdown으로 변환된 문자열을 반환한다.
현재 프로젝트는 Tanstack Query를 사용하고 있어서 mutate()
함수를 통해 서버에 데이터를 전송해주었다. 이는 프로젝트의 특징에 따라 달라질 수 있으니 맞게 변경하면 된다.
4. 📙 입력 값이 없을 때 버튼 비활성화하기
다음으로 입력 값이 없을 때 Submit이 되지 않도록 버튼을 비활성화해보자.
HTML로 입력된 값을 가져오면 initialValue가 공백이어도 기본적으로 <p><br></p>
태그가 들어가있음을 확인할 수 있다.

따라서 에디터에 아무것도 입력하지 않아도 공백이 아닌 태그가 포함된 문자열 데이터가 서버에 전송되어 버린다.
이를 위해서 정규식과 버튼의 disabled 속성을 활용할 것이다.
<typescript />
import "@toast-ui/editor/toastui-editor.css";
import { Editor } from "@toast-ui/react-editor";
interface Props {
editorRef: React.RefObject<Editor>;
onChange: (value: string) => void;
}
export default function TuiEditor({ editorRef, onChange }: Props) {
return (
<>
{editorRef && (
<Editor
ref={editorRef}
height="24rem"
initialValue=" "
initialEditType="wysiwyg"
hideModeSwitch={true}
onChange={onChange}
/>
)}
</>
);
}
우선 Editor 컴포넌트에 onChange 이벤트를 props로 전달받는다.
이는 입력된 값이 변경될 때마다 상태로 저장하기 위해 사용된다.
<typescript />
export default function PostForm() {
// 입력 값을 상태로 관리
const [formData, setFormData] = useState<IPostReq>({
contents: "",
});
// 버튼의 disabled 속성 값을 상태로 관리
const [isDisabled, setIsDisabled] = useState(true);
const { mutate } = useCreatePost();
const editorRef = useRef<Editor>(null);
// 폼 입력값에 따른 작성 버튼 활성화
const validateForm = (data: IPostReq) => {
const { contents } = data;
const cleanedContents = contents.replace(/\s/g, "");
// 태그 외의 텍스트가 있는지 확인
const hasText = /[^\s<>]/.test(cleanedContents);
// 태그로만 이루어진 문자열인지 확인하는 정규식
const isOnlyTag = /^(\<[^>]+\>)+$/.test(cleanedContents);
if (!isOnlyTag && hasText) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
};
// 입력 값이 변경될 때마다 실행될 함수
const handleChangeField = (key: keyof IPostReq, value: string) => {
const contents = editorRef.current?.getInstance().getHTML();
setFormData((prev) => {
const updatedData = { ...prev, [key]: value, contents };
validateForm(updatedData);
return updatedData;
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const contents = editorRef.current?.getInstance().getHTML();
mutate({ ...formData });
};
return(
<form onSubmit={handleSubmit}>
{/* onChange props에 handleChangeField 함수 전달 */}
<TuiEditor
editorRef={editorRef}
onChange={(value) => handleChangeField("contents", value)}
/>
{/* 버튼 컴포넌트에 disabled 속성 추가 */}
<Button
ariaLabel="게시글 작성 완료 버튼"
type="submit"
disabled={isDisabled}
>
작성하기
</Button>
</form>
);
}
4.1. 🔍 코드 뜯어보기
작성 폼에 구현된 코드를 뜯어보도록 하자.
<typescript />
const cleanedContents = contents.replace(/\s/g, "");
const hasText = /[^\s<>]/.test(cleanedContents);
const isOnlyTag = /^(\<[^>]+\>)+$/.test(cleanedContents);
- cleanedContents
replace(/\s/g, "")
를 사용해 공백(\s
)을 모두 제거하여 불필요한 공백을 제거한 콘텐츠
/^(\<[^>]+\>)+$/
- 태그로만 이루어진 문자열인지 확인하는 정규식이다.
<[^>]+>
는 열리고 닫히는 HTML 태그를 매칭한다.
/[^\s<>]/
- 태그 외의 텍스트가 있는지 확인
- 태그(
<>
)나 공백이 아닌 문자가 하나라도 있으면 텍스트가 포함된 것으로 간주한다.
<typescript />
const handleChangeField = (key: keyof IPostReq, value: string) => {
const contents = editorRef.current?.getInstance().getHTML();
setFormData((prev) => {
const updatedData = { ...prev, [key]: value, contents };
validateForm(updatedData);
return updatedData;
});
};
- handleChangeField()
- 폼의 필드 값이 변경될 때마다 실행될 함수
- setFormData()
- 매개변수로 전달 받은
key
와 동일한 객체의 key에value
값을 저장한다. - 이후 validateForm 함수를 호출하고 업데이트된 데이터를 반환한다.
- 매개변수로 전달 받은
- validateForm()
- 업데이트된 데이터를 기반으로 폼의 유효성을 검사하고 버튼 활성화를 조정한다.
여기서 setFormData() 와 validateForm()를 분리하지 않고 setFormData() 내부에서 함수를 호출한 이유는 React의 상태 업데이트 처리 때문이다.
React 에서는 상태 업데이트가 비동기로 처리되기 때문에 handleChangeField
핸들러 안에서 setFormData
로 상태를 업데이트 하게 되면 현재 상태는 이전 값을 참조하게 되어 글자가 하나씩 빠져서 저장되는 것처럼 보이게 된다.
즉, 이전 입력 값이 있었고, 값을 모두 지운 상태임에도 작성 버튼이 활성화된다는 이야기이다.
따라서 setState
함수에 업데이트 함수를 사용((prev) ⇒ …))하여 최신 상태를 안전하게 참조하고 validateForm
을 내부에서 호출하여 최신 상태를 기반으로 동작하게 한다.

Toaust UI Editor는 국내에서 만들어진 에디터로 리드미나 사용 방법들이 자세히 나와있어 구현하는데 큰 어려움이 없었다. 굳이 있었다면 react-editor가 아닌 그냥 editor를 설치했었을 때 정도 ..? 이를 통해 실용적인 에디터를 프로젝트에 넣을 수 있게 되어 좋은 경험이었다 ✨
5. 📙 Reference
tui.editor/apps/react-editor at master · nhn/tui.editor
tui.editor/docs/ko/README.md at master · nhn/tui.editor
'Retrospective > Project' 카테고리의 다른 글
[Tanstack-Query] NextJS14에 Tanstack Query 적용하기 - QueryClient에 useState를 사용하는 이유 (2) | 2024.11.27 |
---|---|
[TailwindCSS] Tailwind CSS 동적 스타일링 적용법 (+ 주의사항) (0) | 2024.11.02 |
[AWS] AWS Lambda + S3 연동해서 파일 가져오기 (0) | 2023.11.15 |
[Vuepress] Vuepress 설치부터 배포 자동화 적용하기 (0) | 2023.09.19 |
[React] 상품/장바구니 수량 조절 기능 구현하기 (0) | 2023.06.09 |