24/2/1

Laejun Kim·2024년 2월 1일
0

TIL

목록 보기
87/89

팀 프로젝트

에디터 적용

어제 글에서 이어지는 내용이다.
오늘도 어제에 이어 에디터 적용 작업을 진행했다.
오늘 처리한 내용은 사진이 base64 포맷으로 저장되는 문제와 뷰어의 스타일링, 사진 업로드 모달의 적용, toolbar의 커스텀 등이다.

뷰어 적용

어제 quill에 뷰어가 따로 없어서 quill의 toolbar를 비활성화 하고 입력 부분을 disable하여 마치 뷰어처럼 사용하겠다는 계획을 밝혔는데, 이렇게 하기 위해선

  1. toolbar 비활성화 하기.
  2. 입력 방지하기

이 두가지를 해야 했다.

먼저 toolbar 를 비활성화 하는 방법은 quill 컴포넌트에 전달하는 modules 객체안에서 toolbar 에 falsy 한 값을 넣어 전달하는 것이다. quill 은 modules 라는 속성을 통해 세부 커스텀이나 설정등을 하게 되는데 이때 toolbar 가 falsy가 되면 화면에 나타나지 않는다.

  const viewModeModules = {
    toolbar: false,
  };

이렇게 생긴 객체를 만든 다음에 이를 quill 컴포넌트에 전달하면 된다.
또한 입력을 막는것은 컴포넌트에 readonly 속성을 추가해 주는것으로 가능하다.

위 두가지를 적용한 컴포넌트의 모습은 아래와 같다

        <QuillNoSSRWrapper
          forwardedRef={quillInstance}
          modules={viewModeModules}
          readOnly
          value={post.content}
        />

여기까지 설정을 마치게 되면 value 에 집어넣은 post.content의 내용을 스타일 그대로 다 적용하여 html 태그를 다 살린채 화면에 뿌려주게 되며 어제 언급한 XSS 걱정도 하지 않아도 되고, 무엇보다 뷰어 컴포넌트를 따로 만들 필요없이 편집/입력 할때도 같은 컴포넌트를 재사용 가능하다. readOnly를 지워주고, modules에 다른 설정 객체를 전달하기만 하면 된다.

Quill 스타일링

뷰어는 잘 동작하는데 한가지 마음에 안드는 점이 있었다. 내가 집어넣은적 없는 border 가 적용되어 있는 것이다. 편집 화면에선 테두리가 있어도 이상할 것이 없지만 그냥 읽기 화면에서는 테두리가 있는 것이 부자연스럽다고 느껴졌다. 또한 글 수정창에서 글의 내용이 짧을 경우 편집기 자체의 높이가 지나치게 낮아지는 문제도 발견, 이 부분도 함께 고쳐보기로 한다.

그런데 공식문서를 찾아봐도 뷰어의 테두리나 높이 관련 설정 하는 부분은 없었다. 그래서 globals.css 에서 직접 제어하기로 한다.

.ql-editor {
  min-height: 300px;
}
.ql-container.ql-snow.ql-disabled {
  border: none;
}

위 코드가 globals.css 에 추가해준 부분인데 viewer 일때만 disabled 속성이 들어가므로 위처럼 코드를 작성해주면 viewer일때만 테두리를 없애는 것이 가능하다. 또한 편집 모드에서 지나치게 높이가 낮아지는것을 막기 위해 300px 의 minimum height를 설정해 주었다.

사진이 base64 로 저장되는 문제점

Quill 에서 기본 제공하는 사진 삽입 tool 을 쓰면 기본적으로 사진을 base64 포멧으로 저장하게 된다. 이렇게 저장된 사진은 아주 긴 하나의 string 으로 변환되어 문서의 img src 부분으로 들어가게 되며 그대로 문서의 내용과 함께 저장된다.

겉보기에는 이렇게 멀쩡해 보이지만 개발자 도구를 열어서 내용을 들여다보게 되면...

실제로는 이런 모양이다. 사실 이것도 너무 길어서 반정도는 화면에 안나오고 잘린 것이다.

이렇게 긴 string이 그대로 글의 본문에 함께 저장되게 되며 이것이 그대로 supabase 테이블에 저장되므로 여러모로 우려스러운 상황.

게다가 코드가 구현되는 구조상 해당 string은 useState를 이용한 state에도 들어갔다 나왔다 하는데, 길이가 지나치게 길어지다 보니 이 상태도 한번씩 오작동 하는 모습을 보였다.

base64 문제 해결

사진을 base64로 저장하지 않으려면 직접 이미지 업로드를 핸들링 하는 방법 밖에는 없다. 이 문제점은 quill 을 사용하는 많은 사람들이 공유하고 있었으며 quill-image-uploader 라는 모듈도 이미 존재하고 있었다. 그러나 해당 모듈은 관리되지 않은지 꽤 오래 되어 보였기 때문에 직접 이미지 핸들링 함수를 만들어 사용하기로 결정하였다.

이미지를 직접 핸들링 하기 위해서는 먼저 module 객체에서 이를 설정해야 한다.

  const editModeModules = useMemo(() => {
    return {
      toolbar: {
        container: [
          ['image'],
          [{ header: [1, 2, 3, 4, 5, false] }],
          ['bold', 'underline', 'italic', 'strike'],
          [{ list: 'ordered' }, { list: 'bullet' }],
          [{ color: [] }],
        ],
      🎇handlers: {
          image: imageHandler,
        },
      },
    };
  }, []);

위에 보이는 것처럼 handlers 를 추가해주고 여기에 사용할 함수를 지정해준다.

imageHandler 함수는 아래와 같다.

  //QUILL 이미지 핸들러
  const imageHandler = async () => {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();
    input.addEventListener('change', async () => {
      setModalOpen(true); //모달 open
      
      //이미지를 담아 전송할 file을 만든다
      const rawfile = input.files?.[0] as File;
      try {
        const options = {
          maxSizeMB: 1, 
          maxWidthOrHeight: 1920,
          useWebWorker: true, 
        };
        const file = await imageCompression(rawfile, options);

        const { data: fileData, error: fileError } = await supabase.storage
          .from('board_images')
          .upload(`${Date.now()}`, file);
        if (fileError) {
          console.error('이미지 업로드 에러', fileError.message);
          return;
        }
        //업로드 된 이미지 url을 가져오기
        const { data: uploadedIMG } = supabase.storage
          .from('board_images')
          .getPublicUrl(fileData.path);
        //에디터의 현재 커서 위치에 이미지 삽입
        const editor = quillInstance!.current!.getEditor();
        const range = editor.getSelection();
        editor.insertEmbed(range!.index, 'image', uploadedIMG.publicUrl);
        setModalOpen(false); //모달 close
      } catch (error) {
        console.error(error);
        toastError('이미지 업로드에 실패했습니다. 다른 이미지로 시도해주세요');
        setModalOpen(false); //모달 close
      }
    });
  };

리뷰 작성 로직을 만들때 사용했던 이미지 압축 코드와 모달 표시/비표시 코드가 함께 들어가 있다.

이렇게 하고나면 게시판 글에 넣은 사진은 supabase storage에 저장되게 되며 여기서 받아온 imgurl을 게시글 내의 img 태그의 src에 삽입하게 된다. 또한 image는 어떤 사이즈를 업로드 해도 항상 1mb 이하로 압축되게 하여서 storge 용량에 대한 염려를 줄였으며 압축되는동안 시간이 몇초간 걸릴 수 있기 때문에 그동안 사용자가 헤메지 않도록 자동으로 스피너 모달이 나타났다가 사라지도록 처리하였다.

현재 에디터 모습


현재는 이렇게 ol, ul, italic, bold 등등 다양한 스타일을 모두 활용 가능하면서 사진도 마음대로 원하는 위치에 첨부할 수 있고 글자색도 원하는대로 바꿀 수 있으며 편집도 가능하고 읽는것도 사용자가 의도한 그대로 보여주는 에디터가 완성되었다. 마음에 들지만 아직 완전히 끝난 것은 아니다...

남은 과제... 사진 삭제는 어떻게..?

지금까지 사진을 업로드하고 업로드된 사진의 포맷을 제어하는데만 열중해서 중요한 문제가 하나 남았다는 것을 알게 되었다. base64를 우회하기 위해 storage에 사진을 올리고 여기서 url을 따오는 것까지는 좋았는데 이제 storage에 올라간 사진을 어떻게 삭제할 것인가?

여기서 삭제는 직접 storage에 들어가서 하나하나 직접 삭제하는 것을 의미하는 것이 아니고 게시글과 함께 삭제되는 것을 말하는 것이다. 현재는 게시글이 삭제되면서 게시글에 등록되어 있던 사진이 함께 삭제되는 기능은 구현되어 있지 않다. 당장은 문제가 되지 않지만 만약에 사용자가 아주 많아진다면, 혹은 시간이 아주 오래 흐른다면 분명 storage의 가용 용량을 모두 사용하게 되는 시점이 올 것이다.

문제는 사진과 게시글을 연결할만한 것이 떠오르지 않는다는 것이다. supabase 테이블에서 어떤 게시글에 해당되는 행이 삭제될때 해당 id를 이용해서 이와 엮여 있는 사진들을 storage에서 지우는 것도 생각해 보았지만 이건 이미 id가 존재할때나 가능한 이야기고 처음 게시글을 작성하면서 사진을 첨부한 경우에는 적용되지 않는 이야기다.

처음 게시글을 작성할때는 해당 게시글을 식별할 수 있는 id가 존재하지 않는 시점이기 때문이다. id가 생기는 것은 게시글 등록 버튼을 누르고 해당 정보가 supabase 테이블에 insert 되었을 때이다. 그런데 사진이 storage에 저장되는 것은 등록 버튼을 누르기 이전, 즉 편집기에 입력을 한창 하고 있는 시점이다.

이 문제를 어떻게 해결해야할지 더 많은 자료를 찾아보고 더 많은 사람들에게 물어 봐야 할 것 같다!

참고한 글
React-Quill 이미지 처리하기 (2)

0개의 댓글

관련 채용 정보