엊그제 대학교에 입학한 것 같은데 어느새 졸업반이 되어버렸습니다 🥲
슬프지만 졸업은 해야하기 때문에 지금 졸업프로젝트를 진행하고 있는데요. 저희는 주제로 일기를 선정했습니다.
그러나 졸업 프로젝트이니만큼 이미 만들어진 라이브러리를 사용하는 건 의미가 퇴색되었고, 때문에 직접 에디터를 개발하게 됩니다. (고통의 시작)
굉장히 심플한 에디터지만 기록을 남기면 좋을 것 같아 간단하게나마 기록을 남겨봅니다 ㅎ.ㅎ
프로젝트 기획 단계에서 저희는 스타일 커스텀이 가능한 일기에 초점을 잡고 개발을 시작하였습니다.
블록 구조
때문에 에디터를 개발할 때도 스타일을 커스텀하기 위해 어떤 식으로 만들고 데이터를 구성하고 서버로 요청을 보낼지 설계하는 데 많은 시간이 걸렸습니다.
에디터 내에 존재하는 각각의 블록(텍스트 블록, 이미지 블록)이 유동적으로 추가, 이동, 삭제가 가능해야 하고 스타일 또한 추가가 가능해야 했기 때문에 고민한 끝에 간단하게 이런 식으로 데이터를 보내기로 결정했습니다.
{
...,
"blocks": [
{
"type": "heading",
"data": {
"text": "블록1의 내용",
"level": 3,
"align": "left"
}
},
{
"type": "img",
"data": {
"link": "이미지url",
"align": "left"
}
},
{
"type": "text",
"data": {
"text": "블록1의 내용",
"align": "left"
}
}
]
}
헤딩은 텍스트 블록에 추가해도 무방하나, 시간이 남는다면 추가 구현할 사항에 목차도 있었어서 미래를 고려하여 따로 빼서 계획했습니다. 근데 생각보다 일정 딜레이가 많이 돼서 아마 프로젝트 중에 개발하기는 힘들지 않을까 생각이 드네요 🤣
다만 이런 식으로 구현한다면 수정할 때마다 블록 배열 전체가 전달되어 완전히 갈아끼워지는 형태가 되기 때문에 성능 이슈가 있지 않을까 우려했습니다.
그러나 저희는 노션처럼 실시간성이 필요하지 않기 때문에 계속 해서 서버와 통신하는 부분이 존재하지 않고(실시간을 추가하더라도 소켓과 같은 형태로 변경하면 괜찮을 것 같았음), 또 '일기'라는 주제에서 벗어나지 않는다면 한 일기가 많은 내용을 포함하지는 않을 것이라고 예상했습니다.
때문에 지금의 구조가 큰 문제가 되지는 않을 것이라고 판단했고, 이 구조를 기반으로 개발에 들어갔습니다.
에디터 디자인
사용자가 봤을 때 사용하기 쉽도록 하려면 기존에 많이 사용되던 디자인을 따라가는 편이 좋다고 생각했습니다. 에디터를 바닥부터 만들어야 하는 상황이었기에 다른 에디터들을 많이 찾아봤는데, 결론적으로 Editor.js와 TUI Editor의 디자인을 참고하여 제작하게 되었습니다.
Editor.js처럼 원하는 부분을 셀렉트 또는 포커스해서 스타일 적용 부분을 커서 밑에 띄워줄 수도 있었으나, 모바일에서 셀렉트 자체가 생각보다 불편했다는 의견이 있었고 이를 받아들여 Toast UI처럼 내비게이터를 추가하는 방식으로 디자인하게 되었습니다.
블록 추가 및 삭제 기능
저희가 관리하는 블록은 세 가지입니다.
- 텍스트 블록
- 헤딩 블록
- 이미지 블록
모든 블록은 사용성을 고려하여 포커스되어 있는 블록의 바로 뒤에 만들어집니다. 다만 헤딩 블록과 이미지 블록은 현재 포커스되어 있는 블록의 내용이 비어있다면 그 블록의 위치에 만들어집니다.
텍스트 블록은 기본 블록으로, shift + enter 또는 블록 앞의 + 버튼 클릭 시 만들 수 있습니다.
헤딩 블록은 select의 형태를 가지고 있으며 각각의 옵션은 h1 ~ h4까지 존재합니다.
처음에는 단순하게 생각하였으나, 이 부분은 구현하면서 한 가지 문제에 직면했습니다. 보통 select는 onChange를 사용하여 값이 바뀌었을 때의 핸들러를 등록해놓는데요. 저희는 h1이 select되어있을 때 h1을 눌러도 동작하도록 만드는 것이 목표였습니다. 때문에 onChange만으로는 원하는 동작을 구현해낼 수 없었고, onFocus 이벤트를 발견하게 되었습니다.
포커스되었을 때 수행될 핸들러에 옵션을 리셋하는 부분을 추가하면 어떨까 생각을 했고, 결과적으로 원하는 방향으로 동작하도록 만들 수 있었습니다.
const headingRef = useRef(null);
const resetHeadingOption = () => {
if (!headingRef.current) return;
headingRef.current.value = 0;
};
return (
<>
...
<select ref={headingRef} onFocus={resetHeadingOption} onChange={addHeadingBlock}>
<option value="1">H1</option>
<option value="2">H2</option>
<option value="3">H3</option>
<option value="4">H4</option>
</select>
</>
);
이미지 블록은 내비게이터에서 이미지 버튼 클릭 시에만 만들 수 있습니다. 이미지 선택 시 이미지 서버에 폼데이터로 감싸진 이미지가 전송이 되고, 그 이미지 링크를 받아 사용자에게 이미지 블록으로 보여주는 형태로 구현되어 있습니다.
텍스트 블록 삭제는 백스페이스가 트리거로 설정되어 있습니다. 커서가 블록의 가장 앞에 있을 때 백스페이스를 누르면 이전 블록에 내용이 합쳐지거나 블록 자체가 삭제됩니다.
이미지 블록은 이미지를 더블 클릭했을 때 이미지를 삭제하겠냐는 컨펌 창이 뜨고 이미지를 삭제할 수 있습니다.
근데 지금 쓰면서 다시 생각해보니 UX 상 너무 불친절했던 것 같습니다.. enter로는 새로운 블록을 만들고 shift + enter로 블록 내 줄바꿈이 가능하도록 만드는 게 훨씬 사용성 측면에서도 나았을 것 같아요... 마찬가지로 삭제도 그냥 바로 삭제할 수 있게 두고 히스토리 기능을 만드는 게 훨씬 나았을 것 같아요 ㅠ_ㅠ
돌아보니 시간에 쫓겨 아쉽게 끝낸 기획이 많았네요 🫠
블록 수정 및 스타일 적용 기능
블록 수정 부분이 가장 큰 걱정이었습니다. 단순히 input을 사용하려고 해도 저희의 제작 목적이 스타일 추가인데 input에서는 태그나 글씨에 스타일 추가가 어려웠기 때문입니다.
그래서 저희가 고려했던 것은 HTML에 내장되어 있는 contenteditable이라는 속성입니다. 다만 이 방식의 문제점은 리액트에서는 입력 중 글자가 씹히거나 커서가 맘대로 이동하는 등의 문제가 있다는 점이었습니다.
그래서 리액트에서 contenteditable을 사용하기 쉽도록 만들어진 라이브러리를 사용하기로 합니다. 바로 react-contenteditable입니다.
소개에는 클래스 컴포넌트에 적용하는 예시가 나와 있지만 함수 컴포넌트에도 적용이 되며, 앞서 말한 리액트에서는 입력 중 글자가 씹히거나 커서가 맘대로 이동하는 등의 문제
를 간단히 해결할 수 있다는 점에서 사용하게 되었습니다.
다만 input처럼 입력하면 내용이 바뀌는 식이 아니라 내부적으로 content를 갈아끼우는 방식이라서, 입력 값을 리액트의 상태로 관리하게 된다면 상태는 비동기로 변경이 되기 때문에 값 반영이 다소 느리게 된다는 단점이 있습니다.
그래서 ref를 사용하였고 스타일 반영 또한 내비게이터 클릭 시 ref.current의 내용을 같이 업데이트하는 방식을 채택할 수밖에 없었습니다.
ref 사용 자체가 나쁘다고 생각하지는 않으나, 블록에 대한 상태를 가지고 있으면서 상태 값을 set할 때 ref의 값을 같이 업데이트해주어야 한다는 점에서 휴먼 에러가 발생할 수도 있고 중복되는 값을 가지고 있다는 점에서 조금의 찜찜함이 남아있기도 합니다. 그러나 당시의 제 지식으로서는 최선을 다했다고 생각합니다 🙃
단순한 내용 작성은 이런 식으로 진행이 됩니다.
만약에 추가적인 스타일 적용이 필요하다면 이런 식으로 적용할 수 있습니다.
블록 이동 기능
블록 이동 자체는 블록들을 배열로 관리하면서 나름 수월하게 구현할 수 있었습니다.
끗
이렇게 해서 저희가 원하는 기능만 제공하는 간단한 에디터가 완성되었습니다. 사실 이렇게 간단하게 말했지만 contenteditable 자체가 문제도 많고, 윈도우와 맥 사이에 다르게 동작하는 또다른 에러들도 있어서 졸프가 끝나기 전까지 계속해서 디버그 중이기도 합니다 😂
react-contenteditable
이 편하지만 한글 사용에 있어 문제점이 많기도 하고 졸업프로젝트이니만큼 에디터를 직접 개발할 수 밖에 없었던 게 아쉽습니다 ㅠ_ㅠ 아니었다면 더 멋진 사이트를 만들 수 있었을텐데,,,
그래도 그동안 도전하지 못했던 새로운 시도였고 직접 개발해보면서 많은 걸 알 수 있었어서 나름대로 즐겁고 뿌듯한 경험이었습니다. 끝! ✨✨✨