기능 구현 웹 에디터 - 04. 추가 기능들.

이유승·2023년 7월 25일
0

기능 구현

목록 보기
16/21
post-custom-banner

1. 기능이 없다?

그런데 상단 툴바를 잘 살펴보니 무슨 이유에서인지 폰트 사이즈를 조정하는 기능이 없다!

유료 버전에만 포함되어 있는건가 싶었는데, 설마 이런 기본적인 기능을 유료로 포함했을 것 같지는 않았고 이미지 삽입도 무료 기능에 포함되어 있는 마당에 폰트 사이즈 조정이 유료일리는 없었다.

공식 문서의 유료 버전 기능에서도 폰트 사이즈에 대한 내용이 없어서 1시간 가량 고민을 했었는데..

const editorInit = {
      max_width: 800,
      max_height: 800,
      menubar: false,
      content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
      fontsize_formats: "8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 30pt 36pt 48pt 60pt 72pt 96pt",
      plugins: [ "image", "code", "table", "link", "media", "codesample", "lists", "autoresize", "codesample" ],
      toolbar:
        'undo redo | fontsize | bold italic backcolor | ' +
        'alignleft aligncenter alignright alignjustify | ' +
        'image | bullist numlist | codesample removeformat ',
};
  • TinyMCE 에디터 설정 파일 예시. (최신 6버전 기준.)

TinyMCE 에디터는 사용하고 싶은 기능에 따라 plugin이 나뉘어져 있고, plugins 속성에 사용할 plugin을 추가한 다음 그에 대응되는 toolbar 키워드를 설정 파일에 입력. 웹 에디터 상에서 기능을 사용하는 방식이다.

조사한 자료에 맞춰 plugins와 toolbar를 적용했음에도 기능이 작동하지 않아 골머리를 썩혔는데, 알고보니 6 버전 이후 에디터의 문법과 사용되는 키워드들의 명칭이 대거 변경된 것이 문제였다. 자료에서는 5버전 이하 내용들이 섞여있었고, 이 내용은 6버전에서는 사용할 수 없어 기능이 동작하지 않았던 것.

6버전에 맞는 자료를 찾아 제대로 된 키워드를 사용했더니, 이제 기능이 정상적으로 동작하기 시작했다.



2. Google FireStore와 연동.

Draft.js와는 달리 TinyMCE 에디터는 데이터 변환 작업 없이 사용자가 작성한 글 데이터를 그대로 DB에 저장할 수 있었다. 블로그에서는 ContextAPI와 useReducer을 사용하고 있으므로 dispatch 함수를 이용하여 state를 관리해주고 있다.

 const addDocument = async (doc) => {
  	dispatch({ type: 'isPending' });
      try {
             const docRef = await addDoc(colRef, { 
                 title: doc.titleData,
                 text: doc.postData,
                 file: fileName,
                 writer: user.displayName,
                 type: doc.selectTypeData,
                 createdTime,
             });

             if (doc.fileData) {
                 const imagesRef = ref(storageRef, fileName);
                 await uploadBytes(imagesRef, doc.fileData);    
             }

             dispatch({ type: 'addDoc', payload: docRef });
             alert('글 작성이 완료되었습니다.');
             navigate('/questions', { replace: true });

        } 
        catch (error) {
            alert(error.message);
            navigate('/questions', { replace: true });
        };
};

기본적인 구조는 에디터에서 작성한 글 데이터가 state의 형태로 저장된 뒤. 파이어스토어의 addDoc 함수를 통해 DB에 데이터를 저장하는 것이다.

저장을 완료하면 사용자에게 글 작성이 완료되었음을 알리고, 게시판 목록으로 화면을 전환한다.

다음 순서는 글 수정 기능이다. 수정 기능 구현에는 DB에 저장된 글 데이터를 가져오는 기능이 필요하다. 이는 글 조회 기능 구현과도 연관이 있다.

const getDocument = async (docid) => {
    dispatch({ type: 'isPending' });
    try {
        const docRef = doc(colRef, docid);
        const docSnap = await getDoc(docRef);
        dispatch({ type: 'getDoc', payload: docSnap.data() });
    } 
    catch (error) {
        dispatch({ type: 'error', payload: error.message });
        alert(error.message);
    }
};

글 데이터를 가져온 다음, 에디터 상에서 데이터를 출력시키고 사용자가 값을 수정한 뒤 다시 저장하도록 한다. 게시판 종류에 따라서 FireStore의 collection를 다르게 지정해야 한다. 따라서 게시판 종류에 따라 collection Ref를 다르게 가져오고, 조건문을 활용하였다.

const updateDocument = async (props) => {
    const docRef = doc(appFireStore, transaction, props.id);

    dispatch({ type: 'isPending' });

    let fileName = 'No file';
    if (props.fileData) {
        fileName = props.fileData.fileName;
    }

    if (props.type === 'qs') {
        try {
            await setDoc(docRef, {
                title: props.titleData,
                text: props.postData,
                fileName: fileName,
                type: props.selectTypeData,
                createdTime,
            }, { merge: true });

            if (props.fileData) {
                const imagesRef = ref(storageRef, fileName);
                await uploadBytes(imagesRef, props.fileData);    
            }

            dispatch({ type: 'updateDoc', payload: docRef });
            alert('글 수정이 완료되었습니다.');
            navigate('/questions', { replace: true });
        } 
        catch (error) {
            alert(error.message);
            dispatch({ type: 'error', payload: error.message });
            navigate('/questions', { replace: true });
        };
    }

(..생략..)

};
  • 게시판 종류에 따라서 setDoc 함수의 동작을 조건문으로 나누어둔 까닭.
    setDoc 함수의 인자로 전달될 글 데이터 객체의 형식과 작업 완료 이후 이동될 게시판 목록의 주소가 다르다. 따라서 조건문을 사용하였다.

그리고 하나의 에디터 컴포넌트에서 글 작성 및 수정을 모두 할 수 있어야 한다. 에디터 컴포넌트로 진입하는 경로에 따라 작성 페이지와 수정 페이지가 동작하도록 해야한다.

useEffect(() => {
    if (type === 'qs' && id !== 'write') {
        setTitleData(response.document?.title);
        setPostData(response.document?.text);
    }
    else if (type === 'dr' && id !== 'write') {
        setTitleData(response.document?.title);
        setPostData(response.document?.text);
    }
    else if (type === 'sr' && id !== 'write') {
        setTitleData(response.document?.title);
        setPostData(response.document?.text);
        setSelectTypeData(response.document?.type);
    };
}, [response]);

이를 위해서 컴포넌트 진입 시 게시판의 종류를 'type' 인자로 받아오고, 글을 작성할 때에는 id값으로 'write'를 수정할 때에는 FireStore가 자동으로 생성한 글 데이터의 id 값을 가져오도록 하여 id값이 'write'가 아닐 경우에는 글 데이터를 조회하여 화면에 출력시키도록 하였다.

  • useEffect와 dependency[]를 이용하면 코드의 작동 시점을 인위적으로 조정할 수 있다.
    []와 같이 빈 dependency만 사용할 경우, 컴포넌트가 렌더링 된 이후에 1회에 한하여 코드가 동작하는데 렌더링 시점과 데이터를 요청하여 받아오는 시점이 맞아떨어지지 않으면 렌더링 이후 데이터가 전송되어 화면에는 아무것도 표시되지 않는 경우가 발생한다. (두 과정의 작동 방식이 서로 비동기이기 때문.) 그런데 [] 내부에 어떤 요소를 넣으면 렌더링 이후에 그 요소가 변화할 때마다 코드가 다시 실행되도록 할 수 있다. 따라서 dependency에 응답 데이터가 담겨져 있는 response 변수를 적용하여 최초 렌더링 이후 response의 값이 변화한다면 다시 코드가 실행되어 화면에 값이 출력되도록 구현하였다.
  • document에 붙은 ?는 무엇인가.
    Optional Chaining. JS에서는 Truthy & Falsy라는 개념으로 어떤 값을 True로 처리할 지 False로 처리할 지에 대한 해답이 제공하고 있다. 요청-응답으로 데이터를 받아오는 것과 화면이 렌더링 되는 것이 알아서 순서를 지켜주는게 아니기 때문에 렌더링 이후 연산이 완료되었을 때 존재하지 않는 값을 처리하는데에 따른 에러가 발생할 수도 있다. 이를 방지하기 위해서는 조건문을 이용하여 결과값이 True에 해당될 경우에만 (null, undefined 등은 false로 간주되기 때문에 값이 실제로 존재할 경우에는 true로 판정되는 점을 이용) 코드가 작동하도록 해주거나, Optional Chaining을 이용하여 값이 없을 경우 JS에서 알아서 undefined라는 값을 적용하여 에러를 방지해줄 수 있다. (undefined라면 빈 화면이 출력되겠지만 값 자체가 존재하지 않으면 에러가 발생되어버린다.)
<button type='submit' className={styles.recordeditorwritebtu}>
    {isUpdate ? <>글 수정</> : <>글 작성</>}  
</button>

그리고 isUpdate 변수와 삼항 연산자를 이용하여 어떤 버튼이 출력될 지 조건 연산을 적용해주고.

const handleOnSubmit = (event) => {
    event.preventDefault();

    if (!isUpdate) {
        addDocument({ type, titleData, postData, fileData, selectTypeData });
    }
    else {
        updateDocument({ type, id, titleData, postData, fileData, selectTypeData });
    }
};

isUpdate 변수을 이용하여 submit 버튼을 클릭했을 때 글 작성 기능이 동작할지, 수정 기능이 동작할 지를 결정해주면 된다.

  • event.preventDefault();
    HTML에서는 기본적으로 submit 작업이 동작할 때, 페이지 새로고침이 일어나도록 기본 설정이 적용되어있다. 그런데 React.js는 SPA 방식으로 새로고침이 일어나는 것을 지양하는 방식의 구조로 이루어져 있다.(페이지가 새로고침 될 때마다 React.js는 state 등이 모두 초기화되어버린다) 따라서 새로고침이 발생해서는 안되는데 이럴 때 사용되는 것이 preventDefault() 함수이다. event에 기본적으로 설정되어 있는 동작이 작동하지 않도록 해준다.

그리고.. 글 수정 기능 또한 정상적으로 동작한다!

profile
프론트엔드 개발자를 준비하고 있습니다.
post-custom-banner

0개의 댓글