WYSIWYG (HTML 에디터) : 게시글 작성을 위한 웹 텍스트 에디터 - TEAM-ARK/inflearn-clone-front GitHub Wiki
- What You See Is What You Get : 보는 대로 얻는다.
- 문서 편집 과정에서 화면에 포맷된 낱말, 문장이 출력물과 동일하게 나오는 방식
inflearn에서 tinymce 에디터를 사용 중인 것 같다.
무료 에디터인지 가격정책을 찾아보니 등급별로 제공되는 혜택이 달랐다.
무료버전 등급을 사용하려 하는데 LGPL 라이센스로 되어 있었고 그것이 무엇인지 찾아보았다.
LGPL은 라이브러리로 사용하여 프로그램을 개발하고 판매/배포할 경우에 소스코드를 공개하지 않아도 되고 LGPL 코드를 사용했음을 명시만 하면 된다.
단, LGPL 코드를 단순히 이용하는 것이 아니라 수정 또는 파생한 라이브러리를 개발하여 배포하는 경우에는 전체 코드를 공개해야 한다.
이 글은 https://blog.hubspot.com/website/best-wysiwyg-html-editor 를 번역해서 정리한 내용으로 오역이나 의역, 개인 의견이 포함되어 있을 수 있습니다.
- 브라우저 기반의 서식이 있는 에디터
- 필요한 콘텐츠 처리 기능을 웹으로 가져올 수 있는 플러그인 기반 아키텍처로 확장 가능합니다.
- 거의 15년 동안 시장에서 CKEditor는 광범위한 기능과 레거시 소프트웨어 호환성을 갖춘 가장 평판이 좋은 편집자 중 하나로서 이 목록에 자리를 잡았습니다.
- 사용자에 따르면 설정의 용이성은 최고의 특성 중 하나입니다. CKEditor의 다른 이점으로는 빠른 로딩(개발 시간 절약)과 수동으로 편집하여 서버에 업로드하지 않고도 프로젝트를 즉시 수정할 수 있는 기능이 있습니다.
- Editor.js는 오픈 소스 편집기입니다. 이동하고 재정렬할 수 있는 콘텐츠 블록을 편집할 수 있습니다.
- 블록을 클릭하면 해당 특정 블록에 사용할 수 있는 특정 옵션이 표시됩니다. 마찬가지로 텍스트 내용을 클릭하면 텍스트 서식 및 인라인 스타일에 대한 옵션이 나타납니다.
- Editor.js는 API(응용 프로그래밍 인터페이스) 덕분에 확장 및 연결 가능하도록 설계되었습니다. 또한 JSON 출력 형식으로 깨끗한 데이터를 반환합니다.
- TinyMCE는 Evernote, Atlassian 및 Medium을 포함한 많은 제품의 뒤에 있는 서식 있는 텍스트 편집기입니다.
- 개발자에 따르면 TinyMCE의 목표는 다른 개발자가 아름다운 웹 콘텐츠 솔루션을 구축할 수 있도록 돕는 것입니다.
- 통합하기 쉽고 클라우드 기반, 자체 호스팅 또는 하이브리드 환경에 배포할 수 있습니다. 설정을 통해 Angular, React 및 Vue와 같은 프레임워크를 통합할 수 있습니다.
- TinyMCE는 표 생성 및 편집, 글꼴 패밀리 설정, 글꼴 검색 및 교체, 글꼴 크기 변경 등의 기능을 사용하여 디자인을 완벽하게 제어할 수 있습니다.
- Quill은 확장 및 사용자 정의를 염두에 두고 구축된 무료 오픈 소스 WYSIWYG 편집기입니다.
- 모듈식 아키텍처와 표현형 API 덕분에 Quill 코어로 시작한 다음 모듈을 사용자 정의하거나 필요에 따라 이 서식 있는 텍스트 편집기에 고유한 확장을 추가할 수 있습니다.
- Quill은 모든 사용자 정의 콘텐츠 및 형식을 지원하므로 포함된 슬라이드 데크, 대화형 체크리스트, 3D 모델 등을 추가할 수 있습니다.
- 이 편집기는 완전히 사용자 정의할 수 있고 더 풍부하고 대화형 콘텐츠를 지원할 수 있기 때문에 개인이 소규모 프로젝트와 Fortune 500대 기업 모두에서 사용합니다.
- Summernote는 Bootstrap 또는 jQuery로 로드할 수 있는 간단한 WYSIWYG 편집기입니다.
- 플러그인을 사용하여 이 편집기를 사용자 정의하고 확장할 수 있습니다.
회원 가입을 하면 API가 발급된다.
- e.g., asdfasdf
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tiny.cloud/1/asdfasdf/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script>
</head>
<body>
<textarea>
Welcome to TinyMCE!
</textarea>
<script>
tinymce.init({
selector: 'textarea',
plugins: 'a11ychecker advcode casechange export formatpainter linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste table advtable tinycomments tinymcespellchecker',
toolbar: 'a11ycheck addcomment showcomments casechange checklist code export formatpainter pageembed permanentpen table',
toolbar_mode: 'floating',
tinycomments_mode: 'embedded',
tinycomments_author: 'Author name',
})
</script>
</body>
</html>
- 발급된 API를 이용해서 tinyMCE WYSIWIG를 사용할 수 있다.
사용 전에 tiny 웹사이트에서 등록한 도메인만 사용이 가능하다.
- API를 사용하지 않거나 도메인에 등록하지 않으면 아래와 같이 표시 된다.
-
https://www.tiny.cloud/docs/integrations/react/#tinymcereactintegrationquickstartguide
-
npm install --save @tinymce/tinymce-react
-
.env 파일 만들기
- REACT_APP_TINYMCE_KEY=[tinyMCE에서 발급받은 API key]
- 리액트에선 환경변수 앞에 항상 'REACT_APP_'을 붙여줘야 인식한다.
- REACT_APP_TINYMCE_KEY=[tinyMCE에서 발급받은 API key]
<>
<Editor
apiKey={process.env.REACT_APP_TINYMCE_KEY}
onInit={(evt, editor) => (editorRef.current = editor)}
initialValue="<p>This is the initial content of the editor.</p>"
init={{
height: 500,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
],
toolbar:
'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
content_style:
'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
}}
/>
<button onClick={log}>Log editor content</button>
</>
이미지 파일을 웹 텍스트 에디터에서 어떻게 업로드하는지 알아보자.
이미지 버튼을 누르면 업로드용 모달창 같은 것이 나온다. 업로드 아이콘을 클릭 후 이미지를 업로드를 하면 다음의 순서대로 업로드가 진행된다.
- method: POST
- content-type: multipart/form-data;
- Request URL:
https://www.inflearn.com/api/files/courses/327576
- [도메인]/api/files/courses/[강의ID]
- Status code : 200
- content-type: application/json;
- data
{
"ok": true,
"id": 170219,
"user_id": 167086,
"course_id": 327576,
"use": "editor",
"name": "렌더링수정전2.PNG",
"type": "image/png",
"size": "79649",
"url": "https://cdn.inflearn.com/public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/렌더링수정전2.PNG",
"storage": "s3",
"s3_info": {
"Key": "public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/렌더링수정전2.PNG",
"key": "public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/렌더링수정전2.PNG",
"ETag": "\"38002127c1060fc6edf794d0b399db62\"",
"Bucket": "ant-man-live",
"Location": "https://ant-man-live.s3.ap-northeast-2.amazonaws.com/public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/%EB%A0%8C%EB%8D%94%EB%A7%81%EC%88%98%EC%A0%95%EC%A0%842.PNG",
"VersionId": "g7f9DQ_rt8ey_sUrI9KHhlO65PqxUONs"
},
"vimeo_info": null,
"vimeo_uploaded": false,
"deleted_file": false,
"created_at": "2021-10-13T14:37:06.857Z",
"updated_at": "2021-10-13T14:37:06.857Z",
"deleted_at": null,
"vod_id": null,
"vod_status": null,
"resource_info": null,
"vod_info": null,
"is_uploaded": true,
"duration": null,
"is_drm": false,
"drm_status": "NOT_CONVERTED"
}
- Request URL을 들어가보니 저장된 이미지가 있는 URL 이다.
- method : GET
- Reqeust URL : 업로드한 이미지가 저장되어 있는 스토리지
- content-type: image/png
- method: POST
- content-type: text/plain;
- Request Payload
[
"38fc19d0-a391-5948-8881-7c2a531963ce",
[
[
"https://cdn.inflearn.com/public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/%EB%A0%8C%EB%8D%94%EB%A7%81%EC%88%98%EC%A0%95%EC%A0%842.PNG",
35,
null
]
]
]
- status code: 200
- Reqeust payload
[
"38fc19d0-a391-5948-8881-7c2a531963ce",
[["/api/files/courses/327576",[349,22],[0,0],200,"POST"]]
]
- Source : https://cdn.inflearn.com/public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/렌더링수정전2.PNG
- Image title: 렌더링수정전2.PNG
- Width / Height : 884 / 739
- save를 누르면 에디터에 이미지가 추가된다
- 추가된 이미지를 눌러보면 img 태그가 에디터 안에 추가되었고 src 속성에서 가리키는 URL은 업로드 후 응답으로 받은 URL인 것을 확인 할 수 있었다.
- 에디터에 사진을 바로 붙여넣기 하는 방식으로 추가했을 땐 서버로 전송되지 않고 브라우저에서 가지고 있다가 base64 방식으로 전달하는 것 같다.
- 이 경우 사진이 많아져도 전송에 문제가 없는지 궁금하다.
-
10개의 사진을 붙여넣기 방식으로 추가후 전송해서 문제가 없는지 테스트 해보았다.
-
문제 없이 전송이 잘 되었다.
-
저장된 페이지로 이동해서 서버에서 받을 땐 어떻게 받아오는지 확인해 보았다.
-
붙여넣기 방식으로 추가한 이미지의 경우 원본 크기를 그대로 살린 것 같아 보였다.
- Intrinsic size : 원본 사이즈
- 그래서 파일 크기에서도 차이가 있었다.
-
이미지 태그에 표시되는 것에도 차이가 있었다.
- 업로드 방식으로 추가한 이미지는 width와 height값 표시되고 파일도 png로 업로드 되었지만, 붙여넣기 방식으로 추가한 이미지는 blob형태로 추가 되었다.
-
업로드 되는 위치는 같았다.
- src="https://cdn.inflearn.com/public/files/courses/327576/7eb203ac-647b-46e4-afc7-60400c95e150/렌더링수정전2.PNG"
- https://cdn.inflearn.com/public/files/courses/327576/d2a5640d-86b3-42cb-86df-369cc130a304/blob
- storage도메인/public/files/courses/[강의id]/[알수없는 폴더]/파일명
-
data-mce-src라는 속성과 src속성에 같은 url이 추가되었다.
- Node.js(and npm)
npm install --save @tinymce/tinymce-react
import React, { useRef } from 'react';
import { Editor } from '@tinymce/tinymce-react';
export default function App() {
const editorRef = useRef(null);
const log = () => {
if (editorRef.current) {
console.log(editorRef.current.getContent());
}
};
return (
<>
<Editor
onInit={(evt, editor) => editorRef.current = editor}
initialValue="<p>This is the initial content of the editor.</p>"
init={{
height: 500,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar: 'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }'
}}
/>
<button onClick={log}>Log editor content</button>
</>
);
}
- apiKey prop에 키를 전달
<Editor apiKey='your-api-key' init={{ /* your other settings */ }} />
- tinymceScriptSrc를 사용하여 React 앱과 독립적으로 TinyMCE를 배포
- tinymceScriptSrc prop에 구체적인 TinyMCE script 경로를 지정
<Editor tinymceScriptSrc="/path/to/tinymce.min.js" />
- 이 외에도 여러가지 self-hosted 방법이 있지만 Tiny Cloud를 이용하는 방식을 사용할 것이기 때문에 이 포스트엔 더 이상 다루지 않음
// npm
$ npm install --save @tinymce/tinymce-react
// yarn
$ yarn add @tinymce/tinymce-react
tinymce-react integration은 다음과 같이 이루어집니다.
- 전역(global) tinymce가 페이지에 있는 경우 그것이 사용됩니다.
- tinymceScriptSrc prop이 있으면 script 태그가 페이지에 추가되어 주어진 URL에서 TinyMCE를 불러옵니다.
- 만약 위 1, 2번 조건들이 적용되지 않았다면, 페이지에 Tiny Cloud로 부터 TinyMCE를 불러오기 위한 스크립트 태그가 추가 됩니다.
다음 props들은 에디터를 구성하는 데 사용됩니다.
-
apiKey : Tiny Cloud API key.
- Tiny Cloud로 부터 불러올 때, 이 prop은 "This domain is not registered..." 경고 메세지를 없애는 데 사용됩니다.
-
cloudChannel : TinyMCE가 Tiny Cloud로 부터 불러올 때 사용되는 채널
-
scriptLoading : 스크립트 로딩 동작 prop
- asnyc와 defer 속성의 세팅을 허용합니다.
- 추가적인 지연 시간 milliseconds 단위로 추가할 수 있습니다.
-
tinyScriptSrc : The URL to use for sourcing TinyMCE, when loading a self-hosted version of TinyMCE
아래 props 들은 react integration이 생성하는 페이지 요소에 대한 일부 제어를 제공합니다.
- 편집기가 초기화되는 요소의 id 속성입니다.
- textarea 태그의 id 매칭되는 부분이고 React에서 사용하는 경우 따로 설정하지 않아도 된다.
- boolean
- inline 속성을 사용한 경우(그냥 보면 일반적인 웹페이지 처럼 보인다.)
- editor로 작성된 page영역을 클릭하면 에디터가 나온다.
- inline 속성을 사용하지 않은 경우, 일반적으로 우리가 생각하는 에디터화면이 나온다.
- 클래식한 iframe 방식의 에디터에서는 무시됩니다.
- 일반적으로 우리가 사용하는 에디터 모양은 tinyMCE에선 iframe방식으로 제공되는 것 같다.
- 다른 예제코드로 해보았던 summernote에선 iframe을 사용하지 않았다.
- iframe에 대한 포스트
// inline과 tagName을 사용한 경우
<Editor
inline
tagName="aTagNameOfEditor"
// ...
/>
- iframe으로 에디터가 들어가지 않고 지정한 태그로 에디터가 들어갔다.
- default 태그는 div 이다.
- textarea 태그(HTML 요소)의 name 속성입니다. 클래식(iframe) 편집기를 만드는 데 사용됩니다. 인라인 편집기에서는 무시됩니다.
- form태그로 에디터를 감싸고 submit 버튼으로 전송하려는 경우에 사용할 수 있을 것 같다.
아래 props들은 에디터가 초기화 될 때 사용됩니다. 에디터가 시작된 후 변경사항들은 무시됩니다.
- TinyMCE가 초기화 될 때 전달되는 추가 옵션
- 에디터 플러그인을 지정
- init prop의 플러그인과 결합됨
- 에디터 도구 모음을 지정
- init prop의 toolbar를 override 함 (여기에서 각종 에디터에 필요한 아이콘을 추가할 수 있는 것 같다.)
아래 props들은 에디터가 초기화된 후 업데이트 될 수 있다.
- 읽기 전용 모드
- 에디터의 시작 값
- 에디터가 로드된 후 이 값을 변경하면 에디터가 재설정 됨(에디터 내용 포함)
- 에디터가 실행취소 단계를 생성하려 할 때 작동하는 이벤트 핸들러
- 필요한 경우 실행취소를 방지하기도 한다.
- 에디터 변화를 감지하는 이벤트핸들러
- TinyMCE가 controlled component로써 구현될 때 유용하다.
- 에디터가 초기화되었을 때 알려주는 이벤트 핸들러
- 에디터의 초기값을 가져올 때 유용
- 제어되지 않는 구성요소에서 사용되는 에디터에 대한 참조를 얻는데 유용
- 에디터의 값을 설정하는데 사용
- 오직 controlled component에서만 사용됨
TinyReact component에서 필수 prop은 없지만 apiKey prop을 구성하지 않은 경우 Tiny Cloud에서 로드할 때 에디터에 대한 경고 메세지가 표시 됨
- 이 도메인은 동록되지 않았습니다... 라는 메세지를 없애는 데 사용(Tiny Cloud를 사용할 때)
- 에디터에 사용되는 TinyMCE빌드를 '특정 버전 또는 안정성 수준을 나타내는 채널'로 변경
- default로 설정된 값 : 5-stable
- 적용 가능한 값들 : 5-stable, 5-testing, 5-dev, 5.10
- 5-stable : TinyMCE의 현재 enterprise release
- 5-testing : TinyMCE의 다음 enterprise release에 대한 현재 후보
- 5-dev : The nightly-build version of TinyMCE.
- A version number such as 5.10: The specific enterprise release version of TinyMCE.
read-only mode(true)와 standard editable mode(false) 중 선택 가능하게 함
- default : false
- tinymce.get('ID') 메소드로 에디터 인스턴스를 검색하는데 사용
- default으로 UUID가 생성 됨
- 에디터를 초기화하는데 사용
- tinymce.init({...}) 메서드로 설정
- basic settup
- tinymce-react의 init은 selector, target, readonly 옵션들이 필요가 없습니다.
- 만약 init에 selector, target, readonly 옵션들이 전달이 되더라도 integration에 의해 재정의 됩니다.
<Editor
init={{
plugins: [
'lists link image paste help wordcount'
],
toolbar: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | help'
}}
/>
- 에디터의 초기 HTML
- initialValue는 에디터의 undo state와 cursor pointer를 재설정합니다.
- 에디터가 로드되기 전 또는 로드 후 비동기 프로세스에 의해 설정될 수 있음
const [initialValue, setInitialValue] = useState(undefined);
useEffect(() => {
// a real application might do a fetch request here to get the content
setTimeout(() => setInitialValue('<p>Once upon a time...</p>'), 500);
}, []);
return (
<Editor
initialValue={initialValue}
/>
);
- 편집기를 인라인 모드로 설정하는 데 사용됩니다.
<Editor inline={true} />
를 사용하는 것은 TinyMCE tinymce.init({...}) 메서드에서 {inline: true}를 설정하는 것과 같습니다.
- TinyMCE의 컴포넌트 밖에서 에디터의 state를 저장하는데 사용 됨
- 이 prop은 TinyMCE React 컴포넌트를 제어 컴포넌트로 사용할 때 일반적으로 사용됩니다
- controlled component vs uncontrolled component 차이?
- value : 현재 에디터의 값(HTML)
- editor : 에디터에 대한 참조(reference)
- Using the TinyMCE React component as a controlled component.
<Editor
plugins='lists code'
/>
- TinyMCE를 로드하기 위해 생성된 스크립트 태그에 async 속성을 설정합니다.
- default value : false
- TinyMCE를 로드하기 위해 생성된 스크립트 태그에 defer 속성을 설정합니다.
- default value : false
<Editor scriptLoading={{ async: true }}>
-
<Editor inline={true} />
인 경우에만 유효합니다. 인라인 모드에서 편집기의 HTML 요소를 정의하는 데 사용됩니다.
<Editor
inline={true}
tagName='section'
/>
- 편집기가 클래식(iframe) 모드일 때만 유효합니다. 양식의 편집기에 사용되는 텍스트 영역 요소의 이름 속성을 설정합니다.
- form 을 사용할거 아니면 안쓸 것 같다.
<form method="post">
<Editor
textareaName='description'
/>
<button type="submit">Submit</button>
</form>
- 에디터의 아이콘들
<Editor
plugins='code'
toolbar='bold italic underline code'
/>
- controlled component로 작동할 때 편집기의 HTML 콘텐츠를 설정합니다.
- 이 prop(:value)이 현재 에디터의 내용과 다르면 에디터 컨텐트(내용)은 200ms안에 맞춰지고 undo level이 생성된다.
- 에디터 값 변경 불가 - 값 변경 안되는데 쓸일이 있을까? inline으로 사용할 경우엔 쓸 수도 있을 것 같다.
- TinyMCE React component는 uncontrolled component로 사용되도록 설계되었다. 더 큰 문서들에서 잘 동작하도록
- onInit 이벤트 핸들러는 콘텐츠 검색을 지원하기 위해 편집기가 로드될 때 편집기 참조를 저장하는 데 사용할 수 있습니다.
- controlled component는 각 키 입력 또는 수정 시 전체 문서를 문자열로 변환해야 하므로 대용량 문서에서 성능 문제가 발생할 수 있습니다. -에디터를 controlled component로 사용하려면 value 및 onEditorChange 소품이 모두 필요합니다.
- value prop은 편집기 내용을 설정하고 재설정하는 데 사용됩니다. 최신 버전의 편집기 콘텐츠로 업데이트되지 않은 경우 편집기는 모든 변경 사항을 롤백합니다.
- onEditorChange prop은 에디터 내용이 변경될 때 실행될 이벤트 핸들러를 제공하는 데 사용됩니다. 변경 사항이 롤백되는 것을 방지하려면 편집기에 대한 변경 사항을 200밀리초 이내에 value prop에 적용해야 합니다.
- docs에 각 props에 대해서 설명을 해주는 부분에 예제 코드가 없어서 이해하는데 너무 불편하다.
- 문서를 처음부터 보다가 중반정도에서 다시 하나씩 자세히 다루는데 이때 예제 코드가 나온다.
- 모든 prop마다 예제가 다 나오는 것은 아니다.
-
https://www.tiny.cloud/docs/integrations/react/#tinymcereactintegrationquickstartguide
-
업로드 시 서버에서 설정해줘야 되는 것들
-
전송 시 에디터에서 설정해줘야 하는 것들
에 대해 알아봐야겠다.
tinyMCE로 이미지를 전송하면 서버에 base64로 전송이 되는 것은 알고 있었는데 텍스트 에디터엔 blob형태로 추가되어서 실제로 base64로 인코딩되어서 전송이 되는지 눈으로 확인해보았다.
- 아이콘을 통해 이미지를 추가 하거나 이미지를 에디터에 직접 붙여넣기 해서 추가하거나 모두 img 태그의 src에 blob형태로 추가가 되는 것 처럼 보였다. 그런데 서버로 직접 전송을 해보니 base64로 변환되어 전송이 되는 것을 확인했다.
- 프론트에선 base64로 전송을 하고 서버에서 따로 저장하는 로직을 추가해야 될 것 같다.
- 업로드 마다 서버에 전송을 해서 추가할 수도 있겠지만 현재 서버가 구축된 상황이 아니고 인프런에서도 이렇게 전송해서 처리하는 것을 확인했기에 이렇게 처리하고 서버 개발자들과 협업할 때 다시 상의를 해봐야될 것 같다.
- 이렇게 이미지를 추가하는 방식의 단점은 인코딩에서 발생한 패딩으로 인해 전송 사이즈가 커질 수 있다.
- 장점은 쓰레기 파일(업로드 했는데 막상 사용되지 않는 이미지 파일)이 생기지 않고 프론트에서 에디터 설정을 추가로 해줄 필요가 없다.