게시글 작성과 수정 기능은 이번 프로젝트에서 가장 구현이 어려웠던 기능 중 하나였다. input 태그 혹은 textarea 태그로 간단하게 구현할 수 있을 것이다라는 생각은 곧 내가 착각을 해도 단단히 착각하고 있었다는 허탈감으로 바뀌었고, 폰트 변경 혹은 폰트 사이즈를 변경하는 것을 시작으로 글 작성 시 필수적으로 들어가는 부가기능들을 어떻게 구현해야하는지는 대략적인 구상조차 쉬운 일이 아니었다.
우여곡절 끝에 웹 에디터 라이브러리를 통해 글을 작성하는 기능 자체는 구현할 수 있었다.
그런데 이후 게시글 작성/수정 컴포넌트를 구현하면서 또 다른 문제에 직면하게 되었다. 게시판은 공부기록과 내용정리의 2종류. 그렇다면 글을 작성할 때 이 2종류를 구분해야 하고, 글을 작성하는 것과 수정하는 것을 또 구분해주어야 했다. 가장 간단한 방법은 글 작성 컴포넌트를 4분할 하는 것이지만 이건 컴포넌트의 재사용성을 뿌리부터 부정하는 것이고, 하나의 컴포넌트에서 이들을 모두 구분하는 방법을 찾아야했다.
우선 가장 처음 해야할 일은 상태 구분용 플래그 변수를 만들어야 한다.
// 글 작성-수정 여부를 제어할 상태 state.
const [isUpdate, setIsUpdate] = useState(false);
// 어떤 종류의 글을 작성하는지 여부를 제어할 상태 state.
const [pageType, setPageType] = useState('dailyrecord');
컴포넌트의 상태가 글 작성인지, 수정인지 구분하는 방법은 다음과 같이 구현하였다.
글을 수정하는 기능은 상세 글 보기 페이지에서 동작하도록 되어있다.
DB에서 필요한 특정 글 데이터를 가져오기 위해서는 기준으로 삼을 값이 필요하다. 기준값은 파이어베이스 서비스에서 데이터를 가져오는게 가능한 값이어야 한다. 따라서 DB 문서의 id값을 기준값으로 설정하였다.
따라서 상세 글 보기 페이지는 url에 DB 문서의 id값이 들어가도록 하고, 글을 조회하는 기능에 사용하는 한편 글을 수정하는 기능에서도 활용하도록 한다. 이 기능은 useParams() 함수를 사용하여 url으로 전달되는 인자를 받아오는 방법으로 구현하였다.
// 이전 컴포넌트에서 전달되는 id 값을 가져온다.
const { type } = useParams();
const { id } = useParams();
useEffect(() => {
// 탭 제목을 제어하기 위해 document의 getElementsByTagName.
const titleElement = document.getElementsByTagName('title')[0];
if (type === 'qs') {
titleElement.innerHTML = '공부기록';
setPageType('questions');
if (id === 'write') {
titleElement.innerHTML += ' - 작성하기';
setIsUpdate(false);
}
else {
titleElement.innerHTML += ' - 수정하기';
setIsUpdate(true);
getDocument(id);
}
};
if (type === 'sr') {
titleElement.innerHTML = '내용정리';
setPageType('studyrecord');
if (id === 'write') {
titleElement.innerHTML += ' - 작성하기';
setIsUpdate(false);
}
else {
titleElement.innerHTML += ' - 수정하기';
setIsUpdate(true);
getDocument(id);
}
};
// eslint-disable-next-line
}, [pageType]);
그렇게 받아온 값을 이용하여 useEffect를 통해 글 작성 여부와 수정 여부를 구분하는 플래그 변수를 설정하고, 글을 수정하는 경우라면 글을 작성하는 UI에 입력된 값을 제어하는 state에 DB에 저장된 게시글 데이터를 가져와서 setState하도록 구현하였다.
글을 새로 작성하는 경우라면, 화면이 이렇게 보이지만..
이미 작성한 글을 수정하는 경우에는 웹 에디터 화면이 이렇게 출력된다. 글 작성과 수정 모두 동일한 컴포넌트에서 동일한 데이터 변수를 공유하고 있으니, 글을 수정할 때에는 페이지가 렌더링 될 때 수정하려는 글의 데이터를 받아와서 setState해주면 되는 것.
useEffect(() => {
// 이전 컴포넌트에서 넘어온 페이지 type을 받아서 조건문 실행.
// 각 조건문 안에서는 또 글 작성인지 수정인지 여부를 확인.
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);
};
// eslint-disable-next-line
}, [response]);
복수의 useEffect 사용.
참조. useEffect는 디펜던시를 사용한다고 해도 컴포넌트 렌더링 시 최초 1번은 무조건 발동하게 된다. 따라서 복수의 useEffect를 사용했을 경우 컴포넌트가 몹시 무거워질 수 밖에 없으니 할 수 있다면 복수의 useEffect를 사용하는 것은 지양해야 한다.
useEffect는 왜 2번 사용되었는가?
-> 첫번째 useEffect의 역할은 글 작성시 어떤 종류의 글을 작성하는 것인지에 대한 플래그 변수가 변화할 경우에 동작하게 된다. 글 종류에 따라서 작성한 글의 세부 분류가 다르고, 글의 종류가 바뀔 때마다 UI가 리렌더링되도록 구현하였다.
{pageType === 'questions' &&
<div className={styles.recordeditoritemsselect}>
<select onChange={handleonChangeSelect} value={selectTypeData}>
<option value='html'>HTML</option>
<option value='css'>CSS</option>
<option value='js'>JS</option>
<option value='js'>TS</option>
<option value='react'>React.js</option>
<option value='next'>Next.js</option>
<option value='redux'>Redux</option>
<option value='firebase'>Firebase</option>
</select>
</div>
}
-> 두번째 useEffect의 역할은 글을 수정하는 경우, DB에 저장된 글 데이터를 받아와서 화면에 렌더링하는 역할을 수행한다. 리액트 특성상 데이터를 받아오기 전에 기본 UI를 먼저 렌더링해버리기 때문에 데이터를 받아온 이후 화면을 갱신해줄 필요가 있기 때문에 useEffect를 사용하였다.
이렇게 하면 글 작성/수정과 공부기록과 내용정리의 상황을 구분하는 기능이 완성된다. isUpdate 플래그 변수를 통해 상황을 정확하게 구분할 수 있게 되었고, 그렇다면 여기에 연결된 다른 기능들의 구현은 그렇게 어려운 일이 아니다.
{/* 작성 혹은 수정 버튼. */}
<button type='submit' className={styles.recordeditorwritebtu}>
{isUpdate ? <>글 수정</> : <>글 작성</>}
</button>
// 글 작성 혹은 수정 버튼을 클릭했을 때 작업을 진행시킬 함수.
const handleOnSubmit = (event) => {
// submit 기능의 기본 기능 (새로고침) 발동을 차단조치.
event.preventDefault();
// isUpdate 여부에 따라서 필요한 함수가 발동되도록.
if (!isUpdate) {
addDocument({ type, titleData, postData, fileData, selectTypeData });
}
else {
updateDocument({ type, id, titleData, postData, fileData, selectTypeData });
}
};
글을 작성하는 버튼은 isUpdate 상태에 따라서 상황에 맞는 글자가 나오도록 하였고, Submit 함수에서도 isUpdate를 활용하여 상황에 맞는 함수가 동작하도록 구현하였다.
평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.
-> 페이지 구분 방법이 모호. 페이지 유형에 대한 레이블이 명확하지 않다.('qs', 'dr', 'sr' 등등..) 코드 유지보수 차원에서 다소 문제의 소지가 있음. 사실 컴포넌트를 구분하는 방법 자체가 약간 혼란스럽게 구현되어 있음. 더 명확한 방법을 찾을 것!
-> 데이터 유효성을 확인하지 않음. 입력한 값이 정상적인지 검사하는 유효성 검사와 정상적인 데이터 형식을 지켰는지 확인하는 절차가 존재하지 않음.
-> 부가 기능 필요. 글을 작성하던 사용자가 페이지를 떠날 경우에 경고창을 띄워준다던가, 작성 중인 글을 임시로 저장하는 등의 기능 구현이 필요함.
백엔드쪽 기능은 파이어베이스를 사용하고 있기에 회원 기능과 구조가 동일하다. 다만 사용되는 함수가 다를 뿐.
const createdTime = timeStamp.fromDate(new Date());
if (doc.type === 'qs') {
try {
const docRef = await addDoc(colRef, {
title: doc.titleData,
text: doc.postData,
writer: user.displayName,
type: doc.selectTypeData,
createdTime,
});
dispatch({ type: 'addDoc', payload: docRef });
alert('글 작성이 완료되었습니다.');
navigate('/questions', { replace: true });
}
catch (error) {
alert(error.message);
navigate('/questions', { replace: true });
};
}
글의 종류에 따라 다른 저장소에 데이터를 저장해야하는데, 그 부분은 조건문을 사용하여 구분하였다. 프론트에서 전달된 입력값을 addDoc 함수를 이용하여 DB에 저장하고, 작업의 성공 혹은 실패 여부에 따라 필요한 동작이 수행되도록 하였다. 글 수정 기능의 경우 함수를 따로 구분하였지만 addDoc 함수가 setDoc 함수로 바뀐 것 뿐이고 기본 구조는 동일하다.
평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.
-> UI 개선점. 기능의 정상동작 여부를 UI에 출력하는 수단이 alert()인데, 간편하기는 하지만 시각적 측면에서 다소 보기 안 좋을 수 있음. 개선 필요. (모달 창 또는 에러 페이지를 추가하는게 좋을지도 모르겠다.)
잘 읽었습니다. 좋은 정보 감사드립니다.