ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [CKEditor5+React] create-react-app으로 생성된 project에 ckeditor5를 추가하는 방법
    ckeditor 2020. 9. 29. 09:18
    반응형


    회사에서 신규 task를 진행하게 됬는데 rich text 기능이 필요했고 찾아보다가 ckeditor5를 선택하였습니다.

    단순히 document가 잘되어 있는것 같아서 ckeditor를 선택하였는데 정작 시작하니 오히려 문서가 너무 많아서 어떻게 봐야할지도 모르겠다는...... ㅠㅠ

    암튼 이렇게 저렇게 삽질하면서 ckeditor5로 필요한 기능은 구현완료 하였고 그간 해왔던 내용을 정리하여 공유합니다.




    ckeditor5를 create-react-app(CRA)에 integrate하는 방법은 official문서에 이미 작성되어 있습니다.

    위의 문서에서 보면 알겠지만 webpack 수정이 필요하기 때문에 yarn eject를 해라고 되어 있습니다.
    eject한 순간부터 CRA를 사용함으로써 얻는 기능들을 사용할 수 없고 다시 eject 전으로 돌아갈수도 없게 됩니다.
    (하기 문장에서는 eject 한 순간부터 판도라의 상자를 연다고 표현했네요. ㅋㅋ)

    eject를 사용하지 않기 위해서 저는 craco를 사용하였습니다.
    craco는 Create React App Configuration Override 로 eject하지 않고 webpack을 수정할 수 있게 하는 lib 입니다.
    craco: https://www.npmjs.com/package/@craco/craco

    그럼 신규 react project를 만드는 것부터 시작해서 integrate 하겠습니다.

    1. CRA를 이용하여 react project를 생성

    npx create-react-app ckeditor-react
    cd ckeditor-react
    npm install

    2. craco와 ckeditor 관련 lib를 다운

    dependencies를 공유합니다.

    "dependencies": {
        "@ckeditor/ckeditor5-alignment""^22.0.0",
        "@ckeditor/ckeditor5-basic-styles""^22.0.0",
        "@ckeditor/ckeditor5-block-quote""^22.0.0",
        "@ckeditor/ckeditor5-dev-utils""^23.5.1",
        "@ckeditor/ckeditor5-dev-webpack-plugin""^23.5.1",
        "@ckeditor/ckeditor5-editor-decoupled""^22.0.0",
        "@ckeditor/ckeditor5-essentials""^22.0.0",
        "@ckeditor/ckeditor5-font""^22.0.0",
        "@ckeditor/ckeditor5-heading""^22.0.0",
        "@ckeditor/ckeditor5-image""^22.0.0",
        "@ckeditor/ckeditor5-indent""^22.0.0",
        "@ckeditor/ckeditor5-link""^22.0.0",
        "@ckeditor/ckeditor5-list""^22.0.0",
        "@ckeditor/ckeditor5-media-embed""^22.0.0",
        "@ckeditor/ckeditor5-paragraph""^22.0.0",
        "@ckeditor/ckeditor5-paste-from-office""^22.0.0",
        "@ckeditor/ckeditor5-react""^2.1.0",
        "@ckeditor/ckeditor5-table""^22.0.0",
        "@ckeditor/ckeditor5-theme-lark""^22.0.0",
        "@craco/craco""^5.7.0",
        "@testing-library/jest-dom""^4.2.4",
        "@testing-library/react""^9.3.2",
        "@testing-library/user-event""^7.1.2",
        "raw-loader""^4.0.1",
        "react""^16.13.1",
        "react-dom""^16.13.1",
        "react-scripts""3.4.3"
      },

    3. craco 설정

    craco.config.js라는 파일을 root 폴더에 생성합니다.(package.json이 있는 폴더)
    해당 파일에서 webpack관련 내용을 수정할 수 있습니다.
    하기와 같이 ckeditor 설정을 추가합니다.

    const CKEditorWebpackPlugin = require("@ckeditor/ckeditor5-dev-webpack-plugin");
    const { styles } = require("@ckeditor/ckeditor5-dev-utils");

    module.exports = {
      webpack: {
        configure: (config, { envpaths }) => {
          config.plugins.push(new CKEditorWebpackPlugin({ language: "en"addMainLanguageTranslationsToAllAssets: true}));

          const regExpThemeIconSvg = /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/;
          const regExpThemeCss = /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css/;
          const cssRegex = /\.css$/;
          const cssModuleRegex = /\.module\.css$/;
          config.module.rules.push(
            { test: regExpThemeIconSvguse: ["raw-loader"] },
            {
              test: regExpThemeCss,
              use: [
                {
                  loader: "style-loader",
                  //   options: { injectType: "singletonStyleTag" }
                },
                {
                  loader: "postcss-loader",
                  options: styles.getPostCssConfig({
                    themeImporter: {
                      themePath: require.resolve("@ckeditor/ckeditor5-theme-lark"),
                    },
                    minify: true,
                  }),
                },
              ],
            }
          );

          config.module.rules.forEach((rule=> {
            if (rule.oneOf) {
              rule.oneOf.forEach((subRule=> {
                if (String(subRule.test) === String(cssRegex)) {
                  subRule.exclude = [
                    cssModuleRegex,
                    regExpThemeCss
                  ];
                }

                if (String(subRule.test) === String(cssModuleRegex)) {
                  subRule.exclude = [regExpThemeCss];
                }

                if (
                  String(subRule.loader).includes("file-loader") &&
                  Array.isArray(subRule.exclude)
                ) {
                  subRule.exclude.push(regExpThemeIconSvgregExpThemeCss);
                }
              });
            }
          });

          return config;
        },
      },
    };



    그리고 package.json의 scripts를 하기와 같이 craco로 수정합니다.
    "scripts": {
        "start""craco start",
        "build""craco build",
        "test""craco test",
        "eject""react-scripts eject"
      },


    4. Editor Component 생성

    Editor Component는 하기와 같이 작성하였습니다.
    config에서 필요한 plugin을 추가하거나 삭제할 수 있습니다.

    import React from "react";
    import CKEditor from "@ckeditor/ckeditor5-react";
    import DecoupledEditor from "@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor";
    import Essentials from "@ckeditor/ckeditor5-essentials/src/essentials";
    import Paragraph from "@ckeditor/ckeditor5-paragraph/src/paragraph";
    import Bold from "@ckeditor/ckeditor5-basic-styles/src/bold";
    import Italic from "@ckeditor/ckeditor5-basic-styles/src/italic";
    import Underline from "@ckeditor/ckeditor5-basic-styles/src/underline";
    import Strikethrough from "@ckeditor/ckeditor5-basic-styles/src/strikethrough";
    import BlockQuote from "@ckeditor/ckeditor5-block-quote/src/blockquote";
    import Link from "@ckeditor/ckeditor5-link/src/link";
    import PasteFromOffice from "@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice";
    import Heading from "@ckeditor/ckeditor5-heading/src/heading";
    import Font from "@ckeditor/ckeditor5-font/src/font";
    import Image from "@ckeditor/ckeditor5-image/src/image";
    import ImageStyle from "@ckeditor/ckeditor5-image/src/imagestyle";
    import ImageToolbar from "@ckeditor/ckeditor5-image/src/imagetoolbar";
    import ImageUpload from "@ckeditor/ckeditor5-image/src/imageupload";
    import ImageResize from "@ckeditor/ckeditor5-image/src/imageresize";
    import List from "@ckeditor/ckeditor5-list/src/list";
    import Alignment from "@ckeditor/ckeditor5-alignment/src/alignment";
    import Table from "@ckeditor/ckeditor5-table/src/table";
    import TableToolbar from "@ckeditor/ckeditor5-table/src/tabletoolbar";
    import TextTransformation from "@ckeditor/ckeditor5-typing/src/texttransformation";
    import Indent from "@ckeditor/ckeditor5-indent/src/indent";
    import IndentBlock from "@ckeditor/ckeditor5-indent/src/indentblock";
    import TableProperties from "@ckeditor/ckeditor5-table/src/tableproperties";
    import TableCellProperties from "@ckeditor/ckeditor5-table/src/tablecellproperties";
    import Base64UploadAdapter from "@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter";

    const Editor = (props=> {
      return (
        <div>
          <CKEditor
            onInit={(editor=> {
              editor.ui
                .getEditableElement()
                .parentElement.insertBefore(
                  editor.ui.view.toolbar.element,
                  editor.ui.getEditableElement()
                );
            }}
            config={{
              language: "ko",
              plugins: [
                Essentials,
                Paragraph,
                Bold,
                Italic,
                Heading,
                Indent,
                IndentBlock,
                Underline,
                Strikethrough,
                BlockQuote,
                Font,
                Alignment,
                List,
                Link,
                PasteFromOffice,
                Image,
                ImageStyle,
                ImageToolbar,
                ImageUpload,
                ImageResize,
                Base64UploadAdapter,
                Table,
                TableToolbar,
                TableProperties,
                TableCellProperties,
                TextTransformation
              ],
              toolbar: props.toolbar
                ? props.toolbar
                : [
                    "heading",
                    "|",
                    "bold",
                    "italic",
                    "underline",
                    "strikethrough",
                    "|",
                    "fontSize",
                    "fontColor",
                    "fontBackgroundColor",
                    "|",
                    "alignment",
                    "outdent",
                    "indent",
                    "bulletedList",
                    "numberedList",
                    "blockQuote",
                    "|",
                    "link",
                    "insertTable",
                    "imageUpload",
                    "|",
                    "undo",
                    "redo",
                  ],
              fontSize: {
                options: [
                  14,
                  15,
                  16,
                  17,
                  18,
                  19,
                  'default',
                  21,
                  22,
                  23,
                  24,
                  25,
                  26,
                  27,
                  28,
                  29,
                  30,
                ],
              },
              alignment: {
                options: ["justify""left""center""right"],
              },
              table: {
                contentToolbar: [
                  "tableColumn",
                  "tableRow",
                  "mergeTableCells",
                  "tableProperties",
                  "tableCellProperties",
                ],
              },
              image: {
                resizeUnit: "px",
                toolbar: [
                  "imageStyle:alignLeft",
                  "imageStyle:full",
                  "imageStyle:alignRight",
                  "|",
                  "imageTextAlternative",
                ],
                styles: ["full""alignLeft""alignRight"],
                type: ["JPEG""JPG""GIF""PNG"],
              },
              typing: {
                transformations: {
                  remove: [
                    "enDash",
                    "emDash",
                    "oneHalf",
                    "oneThird",
                    "twoThirds",
                    "oneForth",
                    "threeQuarters",
                  ],
                },
              },
            }}
            editor={DecoupledEditor}
            {...props}
          />
        </div>
      );
    };

    export default Editor;


    5. CKEditor Viewer 생성

    CKEditor로 작성한 rich text를 보여주기 위해서 하기 페이지의 css를 import해야 합니다.
    https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html

    **여기서 주의할 점**
    CKEditor Component를 import한 파일에서는 위의 css가 없어도 viewer에서 잘 보여줍니다.
    왜냐하면 위의 css가 이미 CKEditor에 포함되어 있기 때문입니다.
    Viewer와 Editor가 다른 페이지에 있을 때(위의 Editor component를 import하지 않는 위치) 위의 css를 import하지 않으면 rich text가 보이지 않습니다.

    Viewer는 다음과 같이 "ck-content"라는 class를 추가하고 dangerouslySetInnerHTML를 사용하면 됩니다.
    const Viewer = ({content}) => (
      <div
        className="ck-content"
        dangerouslySetInnerHTML={__html: content }}
      ></div>
    );

    6. Test Page 생성

    하기와 같이 Test Page 작성하였습니다.
    위에서 말했듯이 Editor를 import했기 때문에 해당 페이지에서는 ckContent.css를 import할 필요가 없지만 저는 그냥 viewer가 있는곳에는 다추가했습니다.

    import React, { useState } from "react";
    import Editor from "./Editor";
    import "./ckContent.css";

    const Viewer = ({content}) => (
      <div
        className="ck-content"
        dangerouslySetInnerHTML={__html: content }}
      ></div>
    );

    const EditorTest = () => {
      const defaultString = "Hello, This is CKEditor~~";
      const [contentsetContent] = useState(defaultString);

      return (
        <div>
          <Editor
            data={content}
            uploadFolder="Test"
            onChange={(eventeditor=> {
              const data = editor.getData();
              setContent(data);
              console.log({ eventeditordata });
            }}
          />
          <Viewer content={content}/>
          <div>{content}</div>
        </div>
      );
    };

    export default EditorTest;


    Source Code: https://github.com/quanjichun/ckeditor-react-integrate


    다음 문장에서는 왜서  DecoupledEditor  를 사용하게 되었는지 간략하게 설명하겠습니다.


    반응형

    댓글

Designed by Tistory.