데브코스 5주차 과정이 진행되는 시점인 10월 17일부터 약 10일 간, 바닐라 자바스크립트만을 이용해 노션을 클로닝하는 프로젝트를 진행하였다. 데브코스에서 진행하는 세 개의 프로젝트 중 첫 번째 프로젝트이며, 두 편에 걸쳐 회고를 작성해보려 한다. 이번 편에서는 프로젝트 준비와 전반적인 구현에 대해 다룰 예정이다.
프로젝트 개발 과정과 전체 코드는 🔗 노션 클로닝 레포지토리에서 확인할 수 있다. 기본적인 구현 내용이나 순서는 본 회고에서 다루지 않았기 때문에, 커밋 로그를 통해 확인하는 것을 추천드립니다 ..
링크를 통해 직접 방문하여 사용해볼 수도 있다.
1. 프로젝트 준비
요구사항
요구사항과 사용할 API에 대한 명세가 주어졌고, 추가적인 기능을 구현하거나 스타일을 변경하는 것은 자율적으로 이루어졌다. 친절한 가이드 덕분에 엄한 부분에서 애를 먹지 않을 수 있었다. 요약해본 요구사항은 다음과 같다.
- 글 단위를 Document라 하며, 화면 좌측에는 Root Documents를 불러오는 API를 통해 Document List를 렌더링한다.
- Root Document에 하위 Document가 있을 경우, 이를 트리 형태로 렌더링한다.
- 편집기에는 기본적으로 저장 버튼이 없으며, Document Save API를 이용하여 지속적으로 서버에 저장되도록 한다.
- History API를 이용해 SPA 형태로 제작한다.
- (보너스) contentEditable을 이용해 마크다운 에디터를 만들어 볼 것
- (보너스) 편집기 하단에 하위 문서를 출력하고, 이를 클릭할 경우 해당 문서로 이동하게 해볼 것
고려사항
이전의 프로젝트나 학습 과정에서 무작정 코드부터 작성하며 느낀 점이 많았기에, 프로젝트 시작 전 고려해볼만한 부분들을 정리하여 이를 계속 의식하며 코드를 작성했던 것 같다. 가독성을 위한 컨벤션 준수, 컴포넌트 구조에 대해 고민을 많이 했다. 물론 잘 지켜지지 못한 부분도 있고, 제대로 한 줄 알았는데 알고 있던 것들과 완전 달랐던 부분들도 있었다. 이는 코드리뷰 이후 개선하여, 다음 회고에 정리할 예정이다.
- ESLint를 통해 Airbnb Convention을 준수하며 코드를 작성한다.
- Commit Convention을 준수하여 기능별로 버전을 관리하고, 작업한 내용을 명확히 기술한다.
- 컴포넌트 간 의존성을 최소화하고, 가능한 작은 단위로 컴포넌트를 구성한다.
- 상태 관리는 최상위 컴포넌트에서 하위 컴포넌트에 내려주는 식으로 구성한다.
컴포넌트 구성
초반에 계획했던 컴포넌트 구조는 다음과 같다. 먼저 페이지별로 분리하였고, 영역별로 컴포넌트들을 세분화하였다. 큰 틀은 비슷하지만 메서드가 합쳐졌거나, 컴포넌트가 더 세분화되었거나 등의 변경사항이 있기에 이 또한 다음 회고에서 함께 다룰 생각이다. 컴포넌트를 나눌 때 스스로 판단해서 나눈 것이기 때문에, 특정 아키텍쳐를 적용해 좀 더 체계적으로 설계해보면 더 좋지 않았을까 생각하였다.
2. 구현
SPA
프로젝트의 가장 큰 특징이라고 볼 수 있다. 하나의 마크업 문서에서 스크립트 파일을 가져와 동적으로 화면을 렌더링하는 SPA 방식으로 프로젝트를 구현하였다. 바닐라 자바스크립트에서 이를 구현하기 위해 부가적인 설정이 필요했다.
HistoryAPI
먼저 History API를 통해 페이지 이동 간 세션 리스토리를 저장하도록 했다. 실제로 페이지가 이동하지는 않지만 URL이 변경될 때마다 세션을 저장하여 히스토리를 업데이트할 수 있게 하였다. 또한 뒤로가기/앞으로가기를 통해 이전 페이지로 복귀할 경우 렌더링 화면이 전부 날아가는 문제가 있었는데, 이를 popstate이벤트로 핸들링하여 추가적인 라우팅이 이루어지게 하였다.
새로고침
페이지를 새로고침할 경우 두 가지의 문제가 발생하였다. 첫 번째로 404에러이다. popstate에서의 문제와 동일하게 변경된 URL에서 새로고침할 경우 해당 URL의 마크업 문서를 요청하여 Not Found가 발생하게 되는데, 이를 위해 사용 중인 서버나 호스팅 서비스에서 추가적인 설정을 통해 최상위 페이지로 재접근하는 등의 대비책을 마련해야했다. 작업 당시엔 http-server에서 npx server -s 커맨드를, Netlify를 통해 배포한 후에는 .redirects 폴더에 index.html을 지정해줌으로써 에러를 방지하였다.
그렇게 404 에러를 해결하고 새로고침을 누르면, 페이지 로딩은 되지만 API 통신으로 갖고 있던 State 정보가 전부 날아갔다. localStorage를 통해 문서의 고유한 Id 값을 key로 부여하고, 라우팅될 때마다 해당 문서의 캐시가 존재하는 경우 이를 불러오도록 해 문제를 해결하였다.
(+ 라우팅 실행 간 Fetch를 하지 않는 것이 원인이라는 의견이 있어 다시 한 번 살펴봐야 할 듯 하다.)
트리 구조 렌더링하기
제일 먼저 작업한 좌측 사이드바의 문서 목록 출력 기능이다. 시작부터 쉽지 않았다. API를 통해 받아온 document의 정보들은 아래와 같이 트리 형태로 구성되어 있었는데, Nested한 Document들을 꺼내서 출력하기 위해 렌더링 함수를 재귀적으로 구성하였다. 문서들을 순회하여 하위 문서가 존재할 경우 renderDocuments 함수를 재귀 호출한다.
[
{
"id": 1,
"title": "문서 제목1",
"documents": [
{
"id": 2,
"title": "하위 문서",
"documents": [
{
"id": 3,
"title": "하위 하위 문서",
"documents": []
}
]
}
]
},
{
"id": 4,
"title": "hello!",
"documents": []
}
]
this.render = () => {
const renderDocuments = (documents) => {
return `
<ul>
${documents
.map(
(document) =>
`<li data-id="${document.id}">
${document.title}
${document.documents.length > 0 ? renderDocuments(document.documents) : ''}
</li>`).join('')}</ul>`;
};
$documentList.innerHTML = renderDocuments(this.state);
};
디바운스를 통한 Status 처리
지난 실습 때 배웠던 디바운스를 프로젝트에 적용해보았다. 애초에 프로젝트의 요구사항이 저장 버튼이 없는, 자동 저장되는 에디터를 만드는 것이기 때문에 글을 작성할 때마다 onEditing 콜백이 끊임없이 호출된다. 지난 회고에도 잠깐 다루었듯 디바운스는 특정 시간 내에 빈번하게 발생하는 이벤트를 하나의 이벤트만 처리하도록 하는 제어 기법이다. 디바운스가 적용된 API 통신이 이루어지고, 통신이 진행되는 동안 스피너를 출력하고 완료된 경우 체크 아이콘을 잠깐 출력하게 하였다. 여담이지만 100ms 아래의 통신 시간이 소요되는 작업들엔 스피너 사용을 권장하지 않는다고 한다. 확실히 없는게 나을 것 같기도 하고.. 한 번 빼봐야겠다.
export default function debounce(callback, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, delay);
};
}
onEditing: debounce(async (editedDocument) => {
const { id, title, content } = editedDocument;
documentEditPage.setState({ ...editedDocument, isSaving: false });
setItem(`temp-document-${id}`, editedDocument);
await modifyDocument({ documentId: id, title, content });
this.setState({ ...this.state, editingDocument: editedDocument });
documentEditPage.setState({ ...editedDocument, isSaving: true });
await this.updateDocumentList();
}, 2000),
디바운스 로직을 외부로 분리하고, 콜백 내부에 문서 저장 로직을 넣어주는 방식으로 구성하였다. 알고보니 이런 방법이 가장 전형적인 디바운스 구현 방식이라고 한다. 유사하지만 다른 개념으로는 쓰로틀이 있는데, 가장 처음 들어온 이벤트만 실행하고 이후 일정 시간 동안 발생한 이벤트를 무시하는 최적화 방법이다. 개념들을 잘 숙지하고 있다가, 필요한 순간에 적절히 활용하면 좋을 듯 하다.
3. 고민했던 부분
다양한 API들로 받아오는 데이터를 통합적으로 관리하기
프로젝트 간 총 문서 리스트를 불러오거나(documents), 한 문서에 대한 상세 정보를 불러오는(editingDocument) 등의 다양한 API를 사용하였다. 사실 그리 많거나 복잡하진 않았지만 하나만 사용하느냐, 하나 이상을 사용하느냐의 문제였던 것 같다. 본 프로젝트에서는 최상위 컴포넌트인 <App />에서 this.state를 통해 데이터들을 관리하고 있는데, 굉장히 허무하게도 객체로 묶어 프로퍼티 형식으로 관리하면 되는 문제였다. 이를 갱신하거나 내려줄 땐 Spread Operator을 이용해 변경할 프로퍼티만 조작해주는 방식으로 여러 개의 데이터를 관리해주었다.
// 각각 받아온 API들을 저장하기 위해 객체 형태로 초기화
const initialState = {
documents: [],
editingDocument: null,
};
// 갱신되는 프로퍼티만 조작
this.setState({ ...this.state, documents: nextState });
// 하위 컴포넌트에 내려줄 때도 필요한 프로퍼티만 전달
sidebar.setState(this.state.documents);
documentEditPage.setState(this.state.editingDocument);
받아온 데이터 외에 추가적인 상태 관리하기
문서 리스트 펼치기/접기 기능을 구현하기 위해서는 받아온 데이터 외에 추가적인 상태(isFolded)를 부여해주어야 했다. 문서 데이터를 Fetching해올 때마다 isFolded 상태를 부여하는 함수를 함께 호출해주었다. 또한 Fetching할 때마다 상태가 초기화되는 것이 아닌 기존 상태값을 기억하고 바뀐 값과 비교하여 합쳐주도록 구현하였다. (setOrToggleIsFolded와 mergerDocuments)
isFolded 상태에 따라 각 문서 이름을 달고 있는 <li> 태그의 클래스를 탈부착하고, 화살표 아이콘을 회전하여 펼치기/접기 기능을 구현해주었다. 애니메이션도 적용하고 싶어 transition을 부여해주었는데, innerHTML DOM 조작을 통해 매번 클래스가 바뀐 채로 렌더링되기 때문에 제대로 작동하지 않았다. 해결 방법을 아시는 분은 댓글 바랍니다...
4. 후기
느낀 점과 배운 점
1. 강의 실습을 진행할 때 확실히 차이점이 느껴졌다. 프로젝트 이전에 비해 실습 간 소요되는 시간도 많이 줄었고, 코드의 흐름을 이해하는 것도 훨씬 쉬워졌다. 강의에서 발생한 에러들도 미리 발견하고 해결한 경우도 종종 있었다. 확보한 시간들로 배운 개념들을 고도화하고자 한다.
2. 구현 이외의 것들도 고려해볼 수 있었던 시간이었다. 이전에 진행했던 개인 프로젝트들은 기능만 작동하면 넘어가고 말았는데, 컨벤션부터 컴포넌트 구조까지 생각해보며 코드를 작성할 수 있어 의미있었던 것 같다. 물론 아직 갈 길이 멀지만, 좀 더 전문성 있는...? 소통에 용이한 코드에 근접하고 있다는 생각이 든달까....?
3. 막상 하면 할만 하다 !
아쉬운 점
1. 막학기 중간고사 이슈로.. 마크업 에디터나, 문서명을 작성하면 링크가 걸리는 등의 구현하지 못한 보너스 요구사항들이 있다. 공식적인 프로젝트 기간은 끝났지만 리팩토링 이후 재밌어 보이는 기능들을 붙혀볼 생각이다.
2. 커밋 컨벤션이나, Linting 같은 부분은 고려하면서 코드를 작성하였지만 스타일링, HTML 렌더링등에서 일관성 없이 작성된 부분들이 많다. 아마 이러한 부분들 때문에 코드가 더 지저분하다고 느껴지는 것 같다. 리뷰 간 소개받은 BEM 컨벤션 등을 이용해 인라인 스타일을 최소화하고, 일관성 있게 코드를 개선해볼 생각이다.
3. 컴포넌트 구조를 분리하거나, 상태를 관리하는 데 있어 스스로 판단하며 진행하였는데 특정 아키텍쳐나 패턴을 적용해보면 좋겠다는 생각을 했다. 다음 프로젝트에서는 이에 더해 왜 그런 방법을 채택했고 어느 점이 좋았는지, 어떤 문제가 발생했는지 등을 정리하면서 진행해봐야겠다.
4. 진행 단계별로 스크린샷을 찍어놓지 않았다는 점도 소소하게 아쉬웠다.
2편에서는 코드리뷰 간 받은 피드백들을 기반으로 리팩토링하고 정리해볼 계획이다. 사실 다음 편이 진짜지 않을까?