Next.js 에서 ckeditor5 를 적용하면서 Image Upload 기능에서 삽질을 많이 했다.
특히, img 태그에 data-id 같은 커스텀 속성을 추가하는 기능을 구현해야 했는데 그 레퍼런스를 찾는 과정에서 많은 어려움이 있었다...
개인 기록용과 더불어서 나와 같이 ckeditor5 의 img 태그 내 커스텀 attribute 추가 기능 관련해서 삽질을 하시는 분이 있으면 도움이 되고자 대충이라도 정리해놓는다..!
next 에 적용하는 방법은 아래 블로그를 참고했다.
에러 발생 내용과 과정이 자세하게 나와있어서 보다 편하게 적용했다!
ckeditor 에서 이미지를 다루다보면 서버에 업로드해서 해당 이미지 주소를 받아와 img 태그의 src 속성으로 넣어주는 과정이 필요할 때가 있다.
그런 경우에는 ckeditor의 config 에 추가적인 plugin 을 만들어서 전달해주면 된다.
일단, 이미지 업로드 커스텀 플러그인 함수를 만든다.
const uploadAdapter = (loader) => {
return {
upload: () => {
return new Promise((resolve, reject) => {
loader.file.then((file) => {
// 만약 서버에 이미지 올리고 받는 등 추가 작업 있을 시 추가해서 이미지 주소를 가져온다.
resolve({
default: '여기에 이미지 주소 넣으면 됩니다.',
});
});
});
},
};
};
const uploadPlugin = (editor) => {
editor.plugins.get('FileRepository').createUploadAdapter = (
loader: any
) => {
return uploadAdapter(loader);
};
};
만든 이미지 업로드 커스텀 플러그인 함수를 ckeditor 컴포넌트 config 속성의 extraPlugins에 추가한다.
<CKEditor
editor={Editor}
data=""
config={{
extraPlugins: [uploadPlugin],
}}
/>
개발을 하다보니 백엔드에 텍스트에디터의 content를 보내줄 때 img 태그 내에 data-id 속성을 추가해서 보내줘야 하는 과정이 있었다.
처음 생각에는 위의 이미지 업로드 커스텀 함수 내부에서 아래와 같이 속성만 추가해서 보내면 되겠지 라고 생각했지만, 실제 img 태그에는 data-id 속성이 추가되지 않았다.
const uploadAdapter = (loader) => {
return {
upload: () => {
return new Promise((resolve, reject) => {
loader.file.then((file) => {
// 만약 서버에 이미지 올리고 받는 등 추가 작업 있을 시 추가해서 이미지 주소를 가져온다.
resolve({
default: '여기에 이미지 주소 넣으면 됩니다.',
// data-id 속성 추가
"data-id": 1234
});
});
});
},
};
};
이리저리 검색해보니 기본적으로 ckeditor 에서는 Image 속성에 적용되는 attribute가 제한되어있어서 위 코드처럼 작성해도 내가 원하는 커스텀(?) attribute 가 추가되지 않는다는 것이었다.
그래서 ckeditor 에서 Image 속성에 내가 원하는 attribute 가 적용이 되도록 추가하는 작업이 필요했다.
ImageUploadEditing 플러그인의 이미지 업로드 완료 이벤트가 발생했을 때 attribute 추가 작업을 실행하면 된다.
(코드 내 속성들은 좀 더 자세히 분석해봐야 할 것 같다..!)
const attrPlugin = editor => {
// 이미지 업로드가 완료되었을 때의 event 를 감지
editor.plugins.get('ImageUploadEditing').on('uploadComplete', (evt, { data, imageElement }) => {
// 1)
editor.model.change(writer => {
writer.setAttribute('dataId', data.dataId, imageElement)
})
// 2)
editor.model.schema.extend('imageBlock', { allowAttributes: 'dataId' })
// 3)
editor.conversion.for('upcast').attributeToAttribute({
view: 'data-id',
model: 'dataId',
})
// 4)
editor.conversion.for('downcast').add(dispatcher => {
dispatcher.on('attribute:dataId:imageBlock', (evt, data, conversionApi) => {
if (!conversionApi.consumable.consume(data.item, evt.name)) {
return
}
const viewWriter = conversionApi.writer
const figure = conversionApi.mapper.toViewElement(data.item)
const img = figure.getChild(0)
if (data.attributeNewValue !== null) {
viewWriter.setAttribute('data-id', data.attributeNewValue, img)
} else {
viewWriter.removeAttribute('data-id', img)
}
})
})
})
}
이 함수 역시 ckeditor 컴포넌트 config 속성의 extraPlugin 에 추가해주면 된다.
<CKEditor
editor={Editor}
data=""
config={{
extraPlugins: [uploadPlugin, attrPlugin],
}}
/>
ckeditor 의 Image 는 ImageBlock 과 ImageInline 두 가지의 속성이 존재한다.
정확하진 않지만 내가 파악하기로는 다음과 같은 형태인 것 같다.
ImageBlock
ImageInline
위의 img 태그에 data attribute 추가하는 과정에서 잘 보면 모두 ImageBlock
에 적용을 하고 있는 것을 볼 수 있을 것이다.
위의 코드는 모두 ImageBlock에만 적용을 했기 때문에 ImageBlock 속성일때만 추가 attribute가 적용되고 ImageInline 속성일때는 추가 attribute 가 적용이 되지 않는 문제가 발생했다.
예를 들어, 가운데 정렬일때는 ImageBlock 속성이라 추가 attribute인 data-id 가 적용이 되고 왼쪽 정렬일때는 ImageInline 속성이라 적용이 안되는 현상이 있었다.
imageInline 은 imageBlock 과는 설정을 약간 다르게 적용해주어야 적용이 되었다.
imageInline 은 dataDowncast
에 attribute를 추가해주고, dispatcher on 설정을 editingDowncast
에서 해주어야 한다.
(자세한 이유는 더 알아봐야겠지만, imageBlock과 imageInline 의 커스텀 속성을 변경할 때 조금 로직이 다르다.. 복잡하다 😭)
const attrPlugin = (editor) => {
// 이미지 업로드가 완료되었을 때의 event 를 감지
editor.plugins
.get('ImageUploadEditing')
.on('uploadComplete', (evt, { data, imageElement }) => {
editor.model.change((writer) => {
writer.setAttribute('dataId', data.dataId, imageElement);
});
editor.model.schema.extend('imageBlock', { allowAttributes: 'dataId' });
// 추가
editor.model.schema.extend('imageInline', {
allowAttributes: 'dataId',
});
editor.conversion.for('upcast').attributeToAttribute({
view: 'data-id',
model: 'dataId',
});
// ✅ imageInline 속성을 위한 추가
editor.conversion.for('dataDowncast').attributeToAttribute({
model: 'dataId',
view: 'data-id',
})
// ✅ imageInline 속성을 위한 추가
editor.conversion.for('editingDowncast').add(dispatcher => {
dispatcher.on('attribute:dataId:imageInline', (evt, data, { writer, consumable, mapper }) => {
if (!consumable.consume(data.item, evt.name)) {
return
}
const imageContainer = mapper.toViewElement(data.item)
const imageElement = imageContainer.getChild(0)
if (data.attributeNewValue !== null) {
writer.setAttribute('data-id', data.attributeNewValue, imageElement)
} else {
writer.removeAttribute('data-id', imageElement)
}
})
})
editor.conversion.for('downcast').add((dispatcher) => {
dispatcher.on(
'attribute:dataId:imageBlock',
(evt, data, conversionApi) => {
if (!conversionApi.consumable.consume(data.item, evt.name)) {
return;
}
const viewWriter = conversionApi.writer;
const figure = conversionApi.mapper.toViewElement(data.item);
const img = figure.getChild(0);
if (data.attributeNewValue !== null) {
viewWriter.setAttribute('data-id', data.attributeNewValue, img);
} else {
viewWriter.removeAttribute('data-id', img);
}
}
);
});
});
};
잘 보았읍니다. 이거 온라인빌더에도 적용 가능 한가요?