Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Jamie] TextEditor 컴포넌트 생성 #56

Merged
merged 12 commits into from
Oct 17, 2022
Merged

[Jamie] TextEditor 컴포넌트 생성 #56

merged 12 commits into from
Oct 17, 2022

Conversation

mina-gwak
Copy link
Member

@mina-gwak mina-gwak commented Oct 12, 2022

close #31

TextEditor

에디터 데모

현재 이미지를 첨부했을 때 msw로 지정된 이미지를 보내주고 있어 스토리북에서는 확인이 불가해 gif를 첨부했습니다!

구현 & 고민사항

에디터 비교

  • 여러 라이브러리들 중 react-quill과 toast-ui/editor 두가지로 추렸고, 처음엔 toast-ui/editor를 선택해서 사용했습니다.
  • 구현 후 다른 분들의 변경사항을 반영하기 위해 리베이스를 했는데, peerDependency가 맞지 않아서 충돌이 났습니다. 패키지를 다시 설치하면서 확인해본 결과 다른 패키지에는 문제가 없었고 toast-ui/editor가 리액트 18 버전을 지원하지 않아서 생긴 문제라는 걸 알게 됐습니다.
    충돌 로그
    toast-ui 내 다른 에디터의 이슈를 보니 리액트 18 버전을 언제 지원할지 알 수 없다고 해서 라이브러리를 변경하기로 했습니다.
  • 두번째 안이었던 react-quill로 다시 구현을 해보았고, 설치 및 사용에 크게 문제가 없어 최종적으로 react-quill을 사용하게 되었습니다.

구현 내용

  • react-quill을 사용하여 에디터를 생성했고, 사용 예시는 스토리를 참고하면 됩니다.
  • 에디터 크기나 기본값, placeholder 등 옵션을 지정할 수 있습니다.
  • 커스텀 이미지 핸들러를 지정했고, 이미지를 선택하면 추가될 이미지 URL을 받아오는 동안 이미지가 업로드중이라는 텍스트를 띄워줍니다.
  • quill-image-resize-module-react 모듈을 사용해 이미지 크기를 조절할 수 있도록 했습니다.
  • GlobalStyle의 reset으로 인해 툴바에서 스타일을 지정해도 적용되지 않아 에디터 내의 텍스트들의 css 속성을 revert했습니다.
  • 에디터 내의 값을 실시간으로 반영할 필요가 없을 것 같다는 점과 상위 컴포넌트가 리렌더링될 때 에디터가 사라지는 문제를 해결하기 위해 비제어 컴포넌트로 구현했습니다.

@netlify
Copy link

netlify bot commented Oct 12, 2022

Deploy Preview for co-studo ready!

Name Link
🔨 Latest commit 28df30d
🔍 Latest deploy log https://app.netlify.com/sites/co-studo/deploys/6348613d6d90c800088cf433
😎 Deploy Preview https://deploy-preview-56--co-studo.netlify.app/
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

Copy link
Contributor

@hemudi hemudi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쩨이미 너무너무 고생 많으셨어요!!!
쨈의 에디터 적용기를 들으면서 외부 라이브러리를 가져와 사용할 때 어떤 고난들을 겪게 되는지...간접 체험했습니다ㅎ....

ReactQuill과 관련한 코드는 해당 라이브러리 사용법에 대해 추가적인 학습을 해야 100% 이해가 갈 것 같아 작성해주신 PR 글을 참고하여 전체적인 흐름만 파악했습니다!
전반적으로 깔끔하게 짜주셔서 흐름을 파악하는데는 큰 무리가 없었어요!
LGTM 이네요! ^_^)b
나중에 시간이 될 때 ReactQuill 에 대해서 저도 찾아보겠습니다ㅎㅎ

코드 잘 봤습니다!
고생하셨어요!!!!

Comment on lines +132 to +148
const modules = useMemo(
() => ({
toolbar: {
container: toolbarOptions,
handlers: {
image: handleImage,
},
},
imageResize: {
parchment: Quill.import('parchment'),
modules: ['Resize', 'DisplaySize', 'Toolbar'],
},
}),
[],
);
Copy link
Contributor

@hemudi hemudi Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 여기에 useMemo 를 쓰신 이유를 여쭤봐도 될까요?
순수한 질문입니다!! ^///^)>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스택오버플로우 글에서는 모듈을 직접 건네주면 키보드를 누를 때마다 모든 모듈을 매번 렌더링되게 만들어서 메모이제이션을 하라고 되어있더라구요! 저같은 경우는 좀 다른 상황이긴 했는데 addRange(): The given range isn't in document 라는 동일한 경고가 떠서 이 부분을 해결하기 위해서 사용했었습니당

Copy link
Member

@healtheloper healtheloper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 커스텀 사용 어렵다!
고생하셨습니다 제이미 💯

Comment on lines +12 to +13
post: <T>(url: string, payload?: object, config?: AxiosRequestConfig) =>
axios.post<ServerResponse<T>>(url, payload, config).then((res) => res.data),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 7 to 14
export const getImageUrl = (image: File) => {
const formData = new FormData();
formData.append('image', image);

return http.post<ImageEntity>(`__API_END_POINT__/image`, formData, {
withCredentials: true,
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결론적으로는 image url 을 얻지만 행위 자체는 이미지를 업로드 하여 그 이미지의 주소를 얻는 것 이므로 getImageUrl 보다는 uploadImage 등의 이름이 어울리지 않을까 생각했습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 네이밍이 좀 걸렸었는데 말씀해주신 이름이 더 나은 것 같네요! 그리고 파크께서 get 함수 작성하신 걸 보면 모두 fetchXX라고 작성하셨던데 하나의 컨벤션인 걸로 보면 될까요? post의 경우 아직 사용된 곳이 없어서 임의로 짓긴 했었습니다,,!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전부 GET 요청이어서 가져온다는 의미로 fetch~ 로 짓긴 했었는데, POST method 의 경우 적절한 네이밍으로 지어도 상관없지 않을까요?
GET 같은 경우는 fetch 로 통일해서 사용하는게 좋아보이고, POST 는 적절한 이름 (apply, upload, create...) 로 쓰여도 상관없지 않을까 싶습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 저도 동의합니당 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

노션에 적어놨숩니당

@@ -0,0 +1,165 @@
import 'react-quill/dist/quill.snow.css';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

css import 위치가 여기에 있음으로 응집도 면에서 이해가 될 수도 있긴 한데,
TextEditor 를 불러와서 사용하는 곳마다 + 한 곳에서 여러번 사용된다면 여러번 호출될까요? (궁금)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 컴포넌트에 두 개의 에디터를 두고 확인해봤는데 여러번 호출되는 것 같지는 않아보였습니다,,! 이런 식으로 한번만 들어가는 것 같아요

에디터 CSS

Comment on lines 68 to 69
width: '800px',
editAreaHeight: '300px',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 rem 기반으로 수정될 필요가 있어보여욥

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 생각엔 default 값이니 반응형 대비해서 width는 %, height는 rem으로 주는게 어떨까 싶어여!
width도 rem으로 주면 많이 확대 시 가로 스크롤이 생길 수 있을거 같네여
아니면 max-width: 100%를 주던지?

width: '100%',
// html이 10px이니 30rem
editAreaHeight: '30rem'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 에디터영역높이라는 의미로 editAreaHeight인건가요..?? 높이 option 같은데 동사가 앞에 있으니 set 하는 함수라고 생각했어요 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jindonyy react-quill 문서에서 editing area 라는 이름을 사용하고 있어서 가져와 썼던 건데 edit area라고 해서 동사처럼 보였을 수도 있겠네요..! 수정하도록 하겠습니당 감사해요 👍

Comment on lines +91 to +101
const contentRef = useRef('');

useImperativeHandle(ref, () => ({
getContents: () => contentRef.current || '',
}));

const handleEditorChange = (content, delta, source, editor) => {
contentRef.current = editor.getHTML();
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentRef 는 여기서 current 값을 바꿔주는데, 결론적으로 어디서 쓰이는지가 안보이는 것 같네요?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentRef는 에디터에서 HTML 형식의 컨텐츠를 가져오는 용도로 사용하고 있습니다!
그냥 단순 텍스트만 가져온다고 하면 ReactQuill에 넘겨주고 있는 editorRef를 사용해 텍스트를 가져오면 되는데, 컨텐츠를 HTML 형식으로 가져오려면 getHTML() 함수를 사용해야 하는데 onChange의 props인 editor에서만 사용할 수가 있더라구요. 그래서 본문에 작성했던 것처럼 에디터가 새로 렌더링되면 사라지는 문제를 해결하기 위해서, 그리고 상태를 사용해 값을 실시간으로 가져올 필요가 없다고 생각해서 contentRef에 onChange 함수에서 가져온 값을 저장하도록 했고 useImperativeHandle에 getContents 함수를 만들어서 ref를 건네주고 있는 상위 컴포넌트가 contentRef 값을 가져갈 수 있도록 작성했습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 useImperativeHandle hook 에 대해서 잘 인지하지 못하고 있어서 제대로 이해하지 못한 거였군영
설명 감사합니다~!

Comment on lines +101 to +110
const handleImage = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');

input.click();

input.addEventListener('change', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event target 이런 것은 사용 불가능하고 createElement 를 해야 하나 보네요??!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 에디터 툴바의 이미지 버튼을 확인해보면 아예 input이 존재하지 않아요,,! 그래서 다들 핸들러 함수 내부에서 input을 생성하는 방식으로 사용하시더라구요 ^_ㅠ

이미지 버튼

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) 혹시 이전에 사용하려던 toast 에디터는 이런 기능이 구현되있던건가여??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jindonyy toast-ui 에디터도 이미지 URL을 새로 받아와서 넣어주려면 이미지 핸들러를 커스텀해서 넣어줘야 했어요! 근데 이제 훅을 등록할 때 들어오는 blob이란 값에 이미지 파일이 담겨있는데, 그걸 가지고 URL을 요청해서 콜백에 넘겨주면 되는 좀 더 간단한 구조였어요

  useEffect(() => {
    if (editorRef.current) {
      editorRef.current.getInstance().removeHook('addImageBlobHook');
      editorRef.current
        .getInstance()
        .addHook('addImageBlobHook', (blob, callback) => {
          (async () => {
            const {
              results: { imageUrl },
            } = await getImageUrl(blob);
            callback(imageUrl, blob.name);
          })();
          return false;
        });
    }
  }, []);

Copy link
Contributor

@jindonyy jindonyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에디터 어렵다 어려워.. 보면서 저도 ref 공부했네요^_ㅠ
라이브러리인데 생각보다 구현해야할게 많네요ㅠㅠ 이전 라이브러리 버전 이슈도 그렇고 고생했어요 제이미! 👍


export const getImageUrl = (image: File) => {
const formData = new FormData();
formData.append('image', image);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormData..ㅇ0ㅇ)!

Comment on lines 7 to 14
export const getImageUrl = (image: File) => {
const formData = new FormData();
formData.append('image', image);

return http.post<ImageEntity>(`__API_END_POINT__/image`, formData, {
withCredentials: true,
});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

노션에 적어놨숩니당

Comment on lines 68 to 69
width: '800px',
editAreaHeight: '300px',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 생각엔 default 값이니 반응형 대비해서 width는 %, height는 rem으로 주는게 어떨까 싶어여!
width도 rem으로 주면 많이 확대 시 가로 스크롤이 생길 수 있을거 같네여
아니면 max-width: 100%를 주던지?

width: '100%',
// html이 10px이니 30rem
editAreaHeight: '30rem'

Comment on lines 68 to 69
width: '800px',
editAreaHeight: '300px',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 에디터영역높이라는 의미로 editAreaHeight인건가요..?? 높이 option 같은데 동사가 앞에 있으니 set 하는 함수라고 생각했어요 🤔

Comment on lines +101 to +110
const handleImage = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');

input.click();

input.addEventListener('change', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) 혹시 이전에 사용하려던 toast 에디터는 이런 기능이 구현되있던건가여??

}
}
});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 여기 eventListener 안에 함수 좀 쪼갤수 있을까여..?? editor 학습이 안되더라도 뭔가 로직은 좀 보였으면 하는데 지금 안에 함수 이름들의 기능을 검색해보면서 봐야 추론이 되는거 같아요. 에디터 기능을 몰라도 함수명으로 로직이 보이면 유지보수할 때 좋을거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 걸 보고 함수로 나눌 수 있을지 좀 생각해봤는데 함수 하나당 하나의 동작을 하는,,? 그런 식이라 분리하는 게 크게 의미가 없을 것 같더라구요. 길어봤자 두줄 정도,,? 어떤 동작을 하는 건지 주석을 추가하면 좀 나을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분리하기 좀 애매하긴 하네여..ㅎㅎ 라이브러리를 활용한거니 주석을 사용해도 좋은거 같아요! 👍

@mina-gwak mina-gwak merged commit 37e052d into dev Oct 17, 2022
@mina-gwak mina-gwak deleted the feat/text-editor/#31 branch October 17, 2022 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Common - TextEditor
4 participants