개주 훈련일지/🏋️ 전집중 호흡 훈련

CKEditor5 커스텀 빌드 만들기(이미지 업로드 구현)

lshfood2 2025. 12. 13. 22:22

커스텀 빌드를 선택했는가?

CKEditor5는 CDN 방식으로도 사용할 수 있다.
하지만 이미지 업로드를

서버(JSP/Servlet)와 연동하려고 하면
CDN 방식은 생각보다 제약이 많다.

 

CDN Super-build의 한계

❌ 불필요한 플러그인이 과도하게 포함됨

❌ 업로드 어댑터를 직접 제어하기 어려움

❌ 서버 연동 시 에러 원인 파악이 힘듦

❌ JSP 환경에서는 디버깅 난이도가 높음

실제로 CDN 방식으로 여러 번 시도했지만,
오류를 해결하는 데 드는 시간이 너무 컸다.

 

커스텀 빌드를 선택한 이유

⭕ 필요한 플러그인만 선택 가능

⭕ SimpleUploadAdapter 사용 가능

⭕ JSP / Servlet 구조에 맞게 설계 가능

 

그래서 CKEditor5 커스텀 빌드를 기준으로 진행했다.
(CDN은 너무 많은 오류를 만나서 포기했다…)


1. 전체 구현 구조 한눈에 보기

[Browser]
   ↓ (multipart/form-data)
CKEditor5
   ↓ POST /uploadImage.do
FrontController (@MultipartConfig)
   ↓
UploadImageAction
   ↓
/upload 폴더에 이미지 저장
   ↓
{ "url": "/day042_totalpractice/upload/xxx.png" } 응답

에디터 + 서버 업로드 + FrontController 구조를

끝까지 연결하는 것이다.


2. CKEditor5 커스텀 빌드 생성

2-1. Node / Git 준비

1) Git 설치부터 시작

CKEditor5 커스텀 빌드는

GitHub 저장소를 기반으로 한다.

 

따라서 Git 설치는 필수다.

▼ Git 다운로드
👉 https://git-scm.com/download/win
(접속 시 자동으로 Windows 버전 다운로드)

설치 과정은 대부분 Next만 눌러도 되지만,
아래 설정은 반드시 체크되어 있어야 한다.

✔ Use Git from the command line and also from 3rd-party software

✔ Use bundled OpenSSH

✔ Checkout Windows-style, commit Unix-style line endings

 

기본값 그대로
Next → Next → Install 하면 문제 없다.

설치 시작 모습

 

2) 설치 후 Git 동작 확인

CMD를 열고 아래 명령어를 입력한다.

git --version

 

버전 정보가 출력되면 Git 설치는 정상이다.

버전이 나오면 성공!

 

3) Node.js 설치

CKEditor5 빌드는 Node.js 환경에서 동작한다.
Node 설치 시 npm은 자동으로 함께 설치된다.

이번 작업에서는 아래 버전을 사용했다.

: node-v18.20.8-x64.msi

노드js 설치 시작 모습

설치 후 CMD에서 확인한다.

버전 확인 명령어를 입력하자

둘 다 출력되면 환경 준비 완료.

 

2-2. CKEditor5 커스텀 빌드 생성

1) 작업 폴더 생성

mkdir C:\ckeditor-build
cd C:\ckeditor-build

커스텀빌드가 생성될 폴더를 만들고

폴더명(=경로)을 입력하자.

 

2) 커스텀 빌드 생성 명령

npx create-ckeditor5-build

위 명령어를 입력하면 폴더에 파일이 생성된다.

이제 커스텀 빌드 기초 파일 제작이 완료되었다.


3. SimpleUploadAdapter 기반 커스텀 설정

설정 핵심 포인트

- Base64 방식 미사용

- 이미지 파일을 multipart/form-data로 서버에 직접 업로드

- 서버는 { "url": "이미지경로" } 만 반환

 

▼ ckeditor.js 핵심 구성

import SimpleUploadAdapter
from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter';

ClassicEditor.builtinPlugins = [
    Image,
    ImageToolbar,
    ImageUpload,
    SimpleUploadAdapter,
    ...
];

ClassicEditor.defaultConfig = {
    toolbar: {
        items: [
            'bold', 'italic',
            'imageUpload',
            'undo', 'redo'
        ]
    }
};

 

빌드 실행 (CMD 입력)

npm run build

위 명령어를 입력하면 내가 설정한 커스텀 설정으로

커스텀 빌드 파일을 제작한다.

명령 실행 완료 모습

 

build 폴더에 ckeditor.js 파일 생성 완료!

이제 해당 파일(=커스텀 빌드)을 프로젝트에 사용하면 된다.


4. JSP 프로젝트에 ckeditor.js 적용

파일 위치 (중요)

src/main/webapp/js/ckeditor.js

생성된 커스텀빌드(ckeditor.js) 파일을

webapp 폴더에 있는 js폴더에 넣어주자.

 

JSP에서 로드

<script src="<%= request.getContextPath() %>/js/ckeditor.js"></script>

내가 사용할 jsp 파일 상단에 로드하면 된다.

 

이 경로 문제로 ClassicEditor is not defined
오류가 가장 많이 발생한다.


5. JSP에서 에디터 초기화 (컨텍스트 경로 핵심)

ClassicEditor
    .create(document.querySelector('#editor'), {
        simpleUpload: {
            uploadUrl: '<%= request.getContextPath() %>/uploadImage.do'
        }
    })
    .then(editor => {
        window.editor = editor;
    });

JSP에서 request.getContextPath()로

업로드 URL을 덮어써야 안전하다.


6. FrontController 설정 (가장 중요한 포인트)

- multipart 업로드의 핵심

multipart 요청은 실제 요청을 받는

서블릿에만 설정해야 한다

 

▼ FrontController.java 코드에 추가

@WebServlet("*.do")
@MultipartConfig
public class FrontController extends HttpServlet {

 

반드시 추가해야 하는 null 처리

ActionForward forward = action.execute(request, response);

if (forward == null) {
    return; // 업로드처럼 응답을 직접 처리한 경우
}

이 한 줄이 없으면 NullPointerException 100% 발생

> 이미지 업로드 액션 페이지에선
> forward 없이 바로 return으로 종료되기 때문!


7. UploadImageAction 구현

package controller.board;

import java.io.File;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

import controller.common.Action;
import controller.common.ActionForward;

public class UploadImageAction implements Action {

    @Override
    public ActionForward execute(HttpServletRequest request, HttpServletResponse response) {

        try {
            request.setCharacterEncoding("UTF-8");

            // CKEditor 업로드 필드명은 반드시 "upload"
            Part filePart = request.getPart("upload");
            if (filePart == null || filePart.getSize() == 0) {
                throw new RuntimeException("업로드 파일이 없습니다.");
            }

            String fileName =
                System.currentTimeMillis() + "_" + filePart.getSubmittedFileName();

            // 저장 경로 (/upload)
            String savePath = request.getServletContext().getRealPath("/upload");
            File uploadDir = new File(savePath);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

            // 파일 저장
            filePart.write(savePath + File.separator + fileName);

            // 브라우저 접근 URL
            String fileUrl = request.getContextPath() + "/upload/" + fileName;

            // CKEditor 규격 JSON 응답
            response.setContentType("application/json; charset=UTF-8");
            response.getWriter().write("{\"url\":\"" + fileUrl + "\"}");

            System.out.println("[이미지 업로드 완료] URL = " + fileUrl);

        } catch (Exception e) {
            e.printStackTrace();
        }

        // ★★★★★ 핵심 ★★★★★
        // 응답을 직접 처리했으므로 forward 사용 X
        return null;
    }
}

필드명은 반드시 upload
응답 JSON은 반드시 { "url": "..." }

처리 완료되면 반드시 return null로 끝난다.


8. 발생했던 대표 오류 정리

ClassicEditor is not defined

- ckeditor.js 경로 오류

404 uploadImage.do

- 컨텍스트 경로 누락

500 Internal Server Error

- @MultipartConfig 누락

- forward == null 처리 없음


[ 이미지 업로드 구현 완료 ]

▼ 게시글 작성 페이지에서 이미지 업로드

게시글 작성 페이지에 이미지를 추가한 모습


게시글 업로드 완료 후 글 상세보기 확인

업로드 완료 후 이미지 노출 확인 완료