이 프로젝트는 노션을 모티브로 한 문서 관리 어플리케이션입니다. 사용자가 문서를 작성하거나 수정하는 순간부터 자동 저장 기능이 활성화되어, 별도의 저장 과정 없이 데이터 보존이 보장됩니다. 이렇게 함으로써, 사용자가 실수로 중요한 정보를 잃어버릴 위험을 최소화하였습니다. 사이드바를 통해 사용자는 작성된 모든 문서의 목록을 한 눈에 볼 수 있으며, 원하는 문서를 즉시 선택하여 열람할 수 있습니다. 또한, 사용자는 필요에 따라 문서를 손쉽게 추가하거나 삭제할 수 있어, 개인적인 노트나 팀 프로젝트의 문서 구성을 자유롭게 조절할 수 있습니다.
- 레포지토리에서
5/#5_kimjaemin_working
브랜치만 클론합니다.
git clone -b 5/#5_kimjaemin_working --single-branch https://github.com/prgrms-fe-devcourse/FEDC5-5_Project_Notion_VanillaJS.git
- dotenv 파일 추가
전역에
.env
파일을 생성하고BASE_URL
환경 변수를 추가합니다.
BASE_URL=https://kdt-frontend.programmers.co.kr
- 필요한 패키지를 설치합니다.
yarn && yarn install
- 로컬에서 실행합니다.
yarn start
📦src
┣ 📂apis
┣ 📂assets
┃ ┗ 📂svg
┣ 📂components
┃ ┣ 📂ChildDocumentLinks
┃ ┣ 📂DocumentItem
┃ ┣ 📂Editor
┃ ┣ 📂NotFound
┃ ┣ 📂Sidebar
┃ ┣ 📂UserGuide
┣ 📂constants
┣ 📂core
┃ ┣ 📂__test__
┃ ┣ 📂hooks
┣ 📂hooks
┣ 📂styles
┣ 📂types
┣ 📂utils
┃ ┣ 📂__test__
┣ 📜App.ts
┗ 📜index.ts
index.ts
: 애플리케이션의 엔트리 포인트입니다. render 함수가 위치해 있습니다.App.ts
: 메인 애플리케이션 컴포넌트입니다.apis
: api와 관련된 함수들이 관리되는 디렉토리입니다.assets
: image, svg 등 정적 파일들을 보관하는 디렉토리입니다.core
: component 생성 관련 함수와 기본 hook들이 위치한 디렉토리입니다.components
: UI 컴포넌트들이 위치한 디렉토리입니다.constants
: 여러 파일에서 사용될 수 있는 상수가 위치한 디렉토리입니다.hooks
: 기본 hook이 아닌 사용자가 정의한 커스텀 hook이 위치한 디렉토리입니다.styles
: style 관련 css 파일들이 위치한 디렉토리입니다.types
: 공통으로 사용되는 타입들이 위치한 디렉토리입니다.utils
: 어플리케이션 전반에서 사용될 util 함수들이 위치한 디렉토리입니다.__test__
: 인접한 파일들의 함수나 로직을 테스트한 파일들이 위치한 디렉토리입니다.
노션 프로젝트에서는 api 호출의 중요성을 깨닫게 되었고, 그에 따른 에러 핸들링에 대한 고민을 하게 되었습니다. 지금까지 aysnc, await를 만나면 무지성으로 try...catch를 해왔는데 이러한 접근 방식이 최선인지에 대한 의문이 들었습니다. api 관련 함수를 추상화 하면서 과정을 여러 단계로 분할하며 async 함수를 선언하게 되었습니다. 이로 인해 내가 작성한 catch 구문이 실제로 어떤 에러를 캐치하는지, 또 내가 throw한 에러가 적절하게 캐치되는지에 대한 의심이 들었습니다. 제가 작성한 api 관련 로직의 흐름을 따라가면 다음과 같은 곳에서 async 함수를 사용하고 있었습니다. 각 단계에서 에러를 전파해야 하는가, 에러를 잡아야 하는가에 대한 고민을 시작했습니다.
apis/core.ts
core 파일의 api 함수는 api 요청을 추상화한 함수입니다. 이 곳에서 모든 api 함수가 필요한 공통적으로 해야할 로직을 정의하고 endpoint와 옵션 값을 파라미터로 받아 응답 데이터를 반환합니다. 여기서 async 함수를 사용했지만 이 곳에서는 try catch문을 사용하지 않았습니다. 이 부분은 요청 함수가 호출됐을때 가장 처음 호출되는 부분으로 요청의 최하위에 해당합니다. 그렇기 때문에 모든 api에서 공통적으로 발생할 수 있는 에러를 한 번에 처리할 수 있습니다. HTTP 상태 코드에 따라 공통적으로 처리할 수 있는 것을 분기를 나눠 처리할 수 있을 것 같습니다. 현 프로젝트에서는 상태 코드에 따라 처리할 것이 없어 단순히 response ok 상태만 확인하여 에러를 던지고 있습니다. 이 부분에서는 에러를 던지고 있지만 catch 하지는 않고 있는데, 그 이유는 요청의 최하위에 해당하기 때문입니다. catch 에러는 하위의 에러를 잡으려고 사용하기 때문에 catch문을 작성해봤자 사용되지 않는 의미없는 구문이 됩니다. 이러한 이유로 core 파일의 api 함수에서는 에러를 던지고 있지만 catch 구문이 존재하지 않습니다.apis/document.ts
이 파일에 있는 api 함수들은 문서와 관련된 api 함수들이 위치합니다. 여기서는 직접적으로 get, post, put, delete 등을 호출합니다. 이 단계에서 에러 전파를 해줄지 말지 고민했습니다. 서버에 에러 로깅을 하는 함수를 호출한다면 여기서 에러 전파가 필요하겠지만 현재는 그런 작업이 필요하지 않고, 대부분의 에러 예외 처리는 UI와 연관되어 있는 경우가 많아 커스텀 훅이나 컴포넌트 단에서 처리하는 것이 더 적절하다고 판단했습니다.hooks/useDocument.ts
이제 커스텀 훅까지 도달했습니다. 해당 커스텀 훅에서fetchDocuments
함수는 에러를 던지지 않지만 나머지 함수들은(생성, 업데이트, 삭제) 에러를 던지고 있습니다. 현재fetchDocuments
는 커스텀 훅 내부에서만 호출하고 있기 때문에 현재 함수가 최상위이므로 에러를 catch 해줄 수 없습니다. 그 외의 함수들은 컴포넌트에서 호출하고 있기 때문에 에러를 throw하고 컴포넌트에서 에러를 잡을 수 있도록 해주었습니다.
이러한 생각의 흐름을 가지고 어디서 에러를 전파하고, 에러 처리를 해줄 것인지에 대해 고민하며 의미없는 try...catch문을 줄일 수 있도록 노력했습니다.
코드를 작성할 때, 특히 복잡한 로직이나 여러 가지 조건들이 교차될때 코드가 정확하게 원하는대로 동작하는지 확인하는 것은 항상 큰 고민거리이자 어려움이었습니다. 처음에는 테스트 코드의 필요성을 크게 느끼지 못했는데 그 상황에서 useEffect
훅의 예상치 못한 동작을 겪게 되었습니다. 이 문제의 원인을 찾기 위해, 객체인지 판별해주는 isObject
함수와 깊은 복사를 수행하는 deepCopy
함수를 테스트 해보았습니다. 전체적인 useEffect
의 동작을 검증하는 것은 복잡한 일이지만 이런 단위 함수들을 테스트 하는 것은 비교적 간단했으며 문제를 빠르게 파악할 수 있었습니다. 이를 통해, 테스트 코드의 가치와 필요성을 명확하게 깨달았습니다.
아직은 어떤 로직들을 테스트 해야할지, 어떤 기준으로 테스트를 진행하는 것이 가장 효율적인지에 대한 확실한 기준을 가지고 있지는 않습니다. 그러나, 테스트 코드를 작성하면서 발생하는 문제의 원인을 훨씬 빠르게 찾게 되었습니다.
앞으로의 프로젝트에서는, 테스트 코드 작성을 일상화하고, 코드의 변경 사항과 추가 기능에 대해 테스트를 진행할 계획입니다. 이를 통해 더욱 견고하고 신뢰성 있는 코드를 작성하고, 나만의 테스트 코드 작성 기준을 세울 수 있을 것이라 기대하고 있습니다.
현재 각 컴포넌트의 이벤트 바인딩은 상위 컴포넌트의 bindEvents
함수를 통해 처리되고 있습니다. 이 방식은 각 컴포넌트가 자신의 상위 컴포넌트에 의존하게 되므로 컴포넌트의 독립성을 저해합니다. useMounted
라는 훅을 구현하여 컴포넌트가 렌더링 된 이후에 이벤트가 바인딩 하려 했으나, 예상대로 작동하지 않았습니다. 렌더링 되기 전에 이벤트 바인딩 함수가 호출되는 문제를 겪었습니다.setTimeout
을 사용해서 useMounted
의 callback 함수의 호출을 지연시켰지만, 이 방법이 항상 돔이 완전히 렌더링 되었음을 보장하지는 못했습니다. 추가적으로, 호출 지연이 가능하다 하더라도 돔 요소가 제거될 때 이벤트 해제 처리는 불가능했습니다.
지금처럼 App 전체가 리렌더링 되는 구조가 아니라 컴포넌트 단위로 렌더링 최적화가 되면 컴포넌트 내에서 이벤트 바인딩이 가능합니다. 렌더링 최적화가 되면 각 컴포넌트는 자신의 생명주기와 로직을 스스로 관리되어 독립성이 높아질 수 있습니다.
하지만 렌더링 최적화를 성공적으로 구현하지 못하였기 때문에, 컴포넌트 내부에서의 이벤트 바인딩이 불가능했습니다. 따라서 눈물을 머금고 구현했던 useMounted
훅을 제거해야만 했습니다.
비동기 로직에는 공통적으로 반복되는 로직들이 많이 발생합니다. 특히 데이터를 가져오는 GET 요청의 결과값은 주로 상태로 관리되며, 이에 따라 로딩 상태나 에러 상태도 함께 관리됩니다. 만약 어플리케이션에서 getDocuments
, getProducts
, getReservation
과 같은 다양한 GET 요청을 한다고 가정해봅시다. 각각의 커스텀 훅을 생성한다면 세 개의 커스텀 훅에서 data, loading, data를 선언하고 상태를 변경하는 로직이 생겨납니다.
이러한 반복되는 로직을 useFetch라는 훅으로 추상화하여 사용하면 코드의 중복을 줄일 수 있으며, 코드의 가독성도 향상될 것이라고 생각하였습니다. tanstack의 useQuery를 참고하여 useFetch를 구현하였으나, 현재의 구조에서 단 하나의 컴포넌트 상태만 변경되어도 전체 컴포넌트가 리렌더링되는 문제로 인해, 여러 컴포넌트에서 useFetch를 사용하면 과도한 리렌더링이 발생하는 문제에 직면하게 되었습니다.
이와 같은 렌더링 최적화의 문제로 결국 useFetch 훅을 제거해야만 했습니다. API 요청과 관련된 상태를 효율적으로 추상화하는 이 방법이 좋아 보였지만, 애플리케이션의 전체 구조 때문에 제거해야 했던 것이 크게 아쉬웠습니다. 처음부터 더 나은 구조로 설계했다면 이러한 문제에 부딪히지 않았을 것이라는 생각이 들었습니다.
웹 접근성 향상을 위해 다양한 노력을 기울였습니다. 시맨틱 태그를 적극적으로 활용하여 웹 컨텐츠의 구조와 의미를 명확하게 표현했습니다. 아이콘과 같은 비텍스트 요소는 시각적으로는 의미를 파악하기 쉽지만 스크린 리더를 사용하는 사용자에게는 그렇지 않기 때문에, 대체 텍스트나 aria-label
속성을 사용하여 해당 요소의 의미를 명확히 전달하였습니다.
스크린 리더 사용자에게 중요한 정보를 제공하면서, 시각적으로 정보를 인식할 수 있는 사용자에게는 불필요한 정보들은 a11yHidden
속성을 사용하여 스크린 리더 사용자만이 해당 정보를 접근할 수 있게 처리하였습니다.
마지막으로, a
태그나 button
태그 외에도 탭 키를 통해 접근이 필요한 요소들에는 tabindex
속성을 부여하여 키보드만을 사용하는 사용자도 웹사이트의 모든 컨텐츠에 접근할 수 있도록 하였습니다.