고급 Props
FroalaEditor의 고급 기능을 제어하는 Props들입니다.
이벤트 핸들러 예제
로딩 중...
이벤트 로그:
editorRef 사용 예제
로딩 중...
editorRef 로그:
커스텀 이벤트 예제
로딩 중...
커스텀 이벤트 로그:
editorRef
- 타입:
<FroalaEditorType | null>
- 기본값:
undefined
- 설명: 에디터 인스턴스에 직접 접근할 수 있는 참조를 제공합니다.
import { useRef } from 'react';
function MyEditor() {
const editorRef = useRef(null);
const handleButtonClick = () => {
if (editorRef.current) {
// 에디터 인스턴스에 직접 접근
const content = editorRef.current.html.get();
console.log('현재 내용:', content);
// 에디터 내용 설정
editorRef.current.html.set('<p>새로운 내용</p>');
// 에디터에 포커스 주기
editorRef.current.focus();
}
};
return (
<div>
<FroalaEditor editorRef={editorRef} />
<button onClick={handleButtonClick}>에디터 제어</button>
</div>
);
}
주요 사용 사례
1. 내용 조작
// HTML 내용 가져오기
const htmlContent = editorRef.current.html.get();
// HTML 내용 설정하기
editorRef.current.html.set('<p>새로운 내용</p>');
// 플레인 텍스트 가져오기
const plainText = editorRef.current.html.get(true);
2. 에디터 상태 제어
// 포커스 주기
editorRef.current.focus();
// 에디터 비활성화/활성화
editorRef.current.edit.off();
editorRef.current.edit.on();
// 전체 선택
editorRef.current.selection.selectAll();
3. 이미지 삽입
// 이미지 URL로 삽입
editorRef.current.image.insert('https://example.com/image.jpg');
// Base64 이미지 삽입
editorRef.current.image.insert('data:image/png;base64,...');
onInit
- 타입:
(editor: FroalaEditorType) => void
- 기본값:
undefined
- 설명: 에디터 초기화가 완료된 후 호출되는 콜백 함수입니다.
const handleInit = (editor) => {
console.log('에디터 초기화 완료');
// 초기화 후 설정 적용
editor.opts.placeholderText = '동적으로 설정된 placeholder';
// 커스텀 툴바 버튼 추가
editor.button.addAfter('bold', 'customButton', {
title: '커스텀 버튼',
icon: 'star',
callback: () => {
editor.html.insert('<span style="color: gold;">⭐</span>');
},
});
};
<FroalaEditor onInit={handleInit} />;
초기화 시 설정 예제
const initializeEditor = (editor) => {
// 툴바 커스터마이징
editor.opts.toolbarButtons = ['bold', 'italic', 'underline'];
// 이벤트 리스너 추가
editor.events.on('keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveContent();
}
});
// 드래그 앤 드롭 설정
editor.events.on('drop', handleFileDrop);
};
onBlur
- 타입:
(editor: FroalaEditorType) => void
- 기본값:
undefined
- 설명: 에디터에서 포커스가 해제될 때 호출되는 콜백 함수입니다.
const handleBlur = (editor) => {
console.log('에디터 포커스 해제');
// 자동 저장
const content = editor.html.get();
saveToLocalStorage(content);
// 유효성 검사
if (content.length < 10) {
showWarning('내용이 너무 짧습니다.');
}
};
<FroalaEditor onBlur={handleBlur} />;
실용적인 onBlur 사용 예제
const [errors, setErrors] = useState([]);
const validateContent = (editor) => {
const content = editor.html.get();
const newErrors = [];
if (content.length < 50) {
newErrors.push('최소 50자 이상 작성해주세요.');
}
if (!content.includes('<img')) {
newErrors.push('이미지를 최소 1개 포함해주세요.');
}
setErrors(newErrors);
};
<FroalaEditor onBlur={validateContent} />;
{
errors.map((error) => (
<div key={error} className="error-message">
{error}
</div>
));
}
licenseKey
- 타입:
string
- 기본값:
undefined
- 설명: Froala Editor의 라이선스 키를 설정합니다.
<FroalaEditor licenseKey="your-license-key-here" value={content} onChange={setContent} />
환경별 라이선스 관리
// 환경 변수로 관리
const licenseKey = process.env.NEXT_PUBLIC_FROALA_LICENSE_KEY;
// 개발/프로덕션 분기
const getLicenseKey = () => {
if (process.env.NODE_ENV === 'development') {
return 'development-license-key';
}
return process.env.FROALA_PRODUCTION_LICENSE_KEY;
};
<FroalaEditor licenseKey={getLicenseKey()} />;
events
- 타입:
Record<string, (...args: any[]) => void>
- 기본값:
undefined
- 설명: 에디터의 다양한 이벤트에 대한 커스텀 핸들러를 설정합니다.
const customEvents = {
// 에디터 초기화 후 이벤트
initialized: function () {
console.log('에디터가 초기화되었습니다');
// this는 FroalaEditor 인스턴스를 가리킵니다
},
// 내용 변경 이벤트 (가장 많이 사용)
contentChanged: function () {
console.log('내용이 변경됨');
const content = this.html.get();
console.log('현재 내용:', content);
},
// 포커스 이벤트
focus: function () {
console.log('에디터에 포커스됨');
},
// 블러 이벤트
blur: function () {
console.log('에디터에서 포커스 해제됨');
},
// 키보드 이벤트
keydown: function (e) {
console.log('키가 눌러짐:', e.key);
// Ctrl+S로 저장 기능
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('저장 단축키 실행');
// saveContent(); // 실제 저장 함수 호출
}
},
// 클릭 이벤트
click: function (e) {
console.log('에디터 클릭됨', e.target);
},
};
<FroalaEditor events={customEvents} />;
실용적인 이벤트 활용 예제
const useEditorEvents = () => {
const [wordCount, setWordCount] = useState(0);
const [unsavedChanges, setUnsavedChanges] = useState(false);
const saveTimeoutRef = useRef(null);
const events = {
// 내용 변경 시 실행
contentChanged: function () {
// 단어 수 계산 (this는 에디터 인스턴스)
const text = this.html.get(true); // 플레인 텍스트로 가져오기
const words = text
.trim()
.split(/\s+/)
.filter((word) => word.length > 0);
setWordCount(words.length);
// 자동 저장 (디바운스 적용)
setUnsavedChanges(true);
// 이전 타이머 취소
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
// 3초 후 자동 저장
saveTimeoutRef.current = setTimeout(() => {
const content = this.html.get();
console.log('자동 저장:', content);
// saveContent(content); // 실제 저장 함수
setUnsavedChanges(false);
}, 3000);
},
// 붙여넣기 전 HTML 정리
'paste.beforeCleanup': function (original_html) {
console.log('붙여넣기 전 HTML 정리');
// 스크립트 태그 제거
let cleaned = original_html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// 스타일 태그 제거 (선택적)
cleaned = cleaned.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
return cleaned;
},
// 이미지 삽입 전 검증
'image.beforeUpload': function (files) {
console.log('이미지 업로드 시작:', files);
// 파일 크기 검증 (5MB 제한)
for (let i = 0; i < files.length; i++) {
if (files[i].size > 5 * 1024 * 1024) {
alert('이미지 크기는 5MB 이하여야 합니다.');
return false; // 업로드 중단
}
}
// 파일 형식 검증
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
for (let i = 0; i < files.length; i++) {
if (!allowedTypes.includes(files[i].type)) {
alert('JPG, PNG, GIF 형식만 업로드 가능합니다.');
return false;
}
}
return true; // 업로드 허용
},
};
return { events, wordCount, unsavedChanges };
};
function EditorWithEvents() {
const { events, wordCount, unsavedChanges } = useEditorEvents();
const [content, setContent] = useState('');
return (
<div>
<div style={{ marginBottom: '10px', fontSize: '14px', color: '#666' }}>
단어 수: {wordCount}
{unsavedChanges && <span style={{ color: 'orange' }}> • 저장되지 않은 변경사항</span>}
</div>
<FroalaEditor value={content} onChange={setContent} events={events} />
</div>
);
}
주요 이벤트 목록
이벤트 이름 | 설명 | 매개변수 |
---|---|---|
initialized | 에디터 초기화 완료 | - |
contentChanged | 내용 변경됨 | - |
focus | 에디터에 포커스 | - |
blur | 에디터에서 포커스 해제 | - |
keydown | 키 누름 | event |
keyup | 키 뗌 | event |
click | 클릭 | event |
paste.before | 붙여넣기 전 | event |
paste.beforeCleanup | 붙여넣기 HTML 정리 전 | html |
image.beforeUpload | 이미지 업로드 전 | files |
image.uploaded | 이미지 업로드 완료 | response |
image.error | 이미지 업로드 오류 | error, response |
이벤트 함수에서
this
사용모든 이벤트 함수에서 this
는 Froala Editor 인스턴스를 가리킵니다.
화살표 함수를 사용하면 this
바인딩이 되지 않으므로 일반 함수를 사용해야 합니다.
기타 Props
viewContentSize
- 설명: 내부적으로 사용되는 설정으로, 콘텐츠 크기 표시 여부를 제어합니다.
- 기본값:
true
tag와 다른 Props들
에디터는 [key: string]: any
타입으로 추가적인 Froala 설정을 받을 수 있습니다:
<FroalaEditor
// Froala 공식 옵션들
language="ko"
theme="royal"
toolbarInline={true}
charCounterCount={false}
// 커스텀 Props
value={content}
onChange={setContent}
/>
💡 고급 사용 팁
1. 에디터 인스턴스 상태 관리
const useEditorState = () => {
const editorRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const handleInit = (editor) => {
setIsReady(true);
};
return { editorRef, isReady, handleInit };
};
2. 동적 툴바 설정
const [toolbarMode, setToolbarMode] = useState('full');
const getToolbarButtons = () => {
switch (toolbarMode) {
case 'simple':
return ['bold', 'italic', 'underline'];
case 'full':
return ['bold', 'italic', 'underline', 'insertImage', 'insertTable'];
default:
return [];
}
};
<FroalaEditor
toolbarButtons={getToolbarButtons()}
onInit={(editor) => {
editor.toolbar.refresh();
}}
/>;
3. 에러 핸들링
const handleInit = (editor) => {
try {
// 에디터 설정
editor.opts.imageUploadURL = '/api/upload';
} catch (error) {
console.error('에디터 초기화 오류:', error);
showErrorNotification('에디터 초기화에 실패했습니다.');
}
};
툴바 옵션
에디터의 툴바를 다양한 화면 크기에 맞게 개별적으로 설정할 수 있습니다.
툴바 Props
- toolbarButtons: 데스크톱 화면 (≥ 1200px)의 툴바 버튼
- toolbarButtonsMD: 중간 크기 화면 (≥ 992px)의 툴바 버튼
- toolbarButtonsSM: 작은 화면 (≥ 768px)의 툴바 버튼
- toolbarButtonsXS: 매우 작은 화면 (< 768px)의 툴바 버튼
모든 툴바 옵션은 기본적으로 TOOLBAR_CONFIG_OPTIONS
의 설정을 가지고 시작합니다.
TOOLBAR_CONFIG_OPTIONS 구성
{
moreText: {
buttons: [
'paragraphFormat',
'fontFamily',
'fontSize',
'textColor',
'backgroundColor',
'bold',
'italic',
'underline',
'strikeThrough',
'subscript',
'superscript',
'clearFormatting',
],
buttonsVisible: 4,
},
moreParagraph: {
buttons: [
'formatOL',
'formatUL',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'lineHeight',
'outdent',
'indent',
'quote',
],
buttonsVisible: 2,
},
moreRich: {
buttons: ['insertImage', 'insertLink', 'insertTable', 'emoticons', 'specialCharacters', 'insertHR'],
buttonsVisible: 2,
},
moreMisc: {
buttons: ['undo', 'redo', 'fullscreen', 'selectAll', 'print', 'html', 'pastePlain'],
align: 'right',
buttonsVisible: 0,
},
}
import { FroalaEditor } from '@ncds/editor';
// 반응형 툴바 설정 예제
<FroalaEditor
value={content}
onChange={setContent}
// 데스크톱 전체 툴바
toolbarButtons={[
'bold',
'italic',
'underline',
'strikeThrough',
'fontFamily',
'fontSize',
'color',
'backgroundColor',
'insertTable',
'insertImage',
'insertVideo',
'insertLink',
]}
// 태블릿용 축소 툴바
toolbarButtonsMD={['bold', 'italic', 'underline', 'fontSize', 'color', 'insertImage', 'insertLink']}
// 모바일용 최소 툴바
toolbarButtonsSM={['bold', 'italic', 'underline', 'insertImage', 'insertLink']}
// 매우 작은 화면용 기본 툴바
toolbarButtonsXS={['bold', 'italic']}
/>;
툴바 커스터마이징 예제
const ToolbarCustomExample = () => {
const [content, setContent] = useState('');
// 기본 TOOLBAR_CONFIG_OPTIONS를 기반으로 한 커스텀 설정
const customToolbarButtons = [
'bold',
'italic',
'underline',
'strikeThrough',
'|',
'fontFamily',
'fontSize',
'|',
'color',
'backgroundColor',
'|',
'formatOL',
'formatUL',
'outdent',
'indent',
'|',
'insertImage',
'insertTable',
'insertLink',
'|',
'undo',
'redo',
];
const mobileToolbarButtons = [
'bold',
'italic',
'underline',
'|',
'fontSize',
'color',
'|',
'insertImage',
'insertLink',
];
return (
<FroalaEditor
value={content}
onChange={setContent}
toolbarButtons={customToolbarButtons}
toolbarButtonsMD={customToolbarButtons}
toolbarButtonsSM={mobileToolbarButtons}
toolbarButtonsXS={['bold', 'italic', 'insertImage']}
/>
);
};
툴바 구분자
툴바 버튼 배열에서 '|'
를 사용하여 버튼 그룹을 구분할 수 있습니다.