[Mission4/김규란] - Project_Notion_Vanilla_JS#12
[Mission4/김규란] - Project_Notion_Vanilla_JS#12gxxrxn wants to merge 9 commits intoprgrms-fe-devcourse:3/#4_gxxrxnfrom
Conversation
- gitignore 설정
- Sidebar 컴포넌트 - SidebarSection > Details > DocumentItem - SidebarFooter
rjsduf0503
left a comment
There was a problem hiding this comment.
안녕하세요 규란님 :)
이번 노션 프로젝트도 수행하느라 고생 많으셨습니다 ㅎㅎ
어느새 팀원으로써 마지막 코드 리뷰를 남기게 되었는데 상당히 아쉽네요!
리뷰할 때마다 느끼는거지만 규란님은 항상 코드를 굉장히 효율적으로 짜시는 것 같아 부럽습니다.. 재밌게 잘 읽었습니다!
| const addEvent = ($target, eventType, selector, callback) => { | ||
| const children = [...$target.querySelectorAll(selector)]; | ||
| const isTarget = (target) => children.includes(target) || target.closest(selector); | ||
|
|
||
| $target.addEventListener(eventType, (event) => { | ||
| if (!isTarget(event.target)) return false; | ||
| callback(event); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
event 추가하는 로직을 따로 빼내서 더 간결하게 짜셨군요.. 한 수 배우고 갑니다 :)
There was a problem hiding this comment.
저도 배워야 할 점 같습니다. 저는 addEventListener를 엄청 남발해뒀는데 이렇게 로직을 따로 빼두면 코드를 더 간결하게 만들 수 있겠네요. 한 번 써먹어 보겠습니다!
| import { routes } from "./constants/routes.js"; | ||
|
|
||
| export default function App({ $target }) { | ||
| this.$target = $target; |
There was a problem hiding this comment.
ask) 코드를 읽어보니 모두 this.$target = $target 으로 초기 설정을 하셨는데 이유가 뭔가요????? 궁금합니다.. 뭔가 규란님이라면 이유가 있어서 하셨을 것 같아서요 !!
| export const USER = { | ||
| NAME: "규란", | ||
| PROFILE_URL: | ||
| "https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1603679366/noticon/dcvetqndre7gda3ttijy.gif", |
There was a problem hiding this comment.
ㅋㅋㅋㅋㅋ 무슨 프로필인가 궁금해서 들어갔는데 움직이는 강아지네요 웃고 갑니다
| @@ -0,0 +1,24 @@ | |||
| export default function Component({ $target, initialState }) { | |||
There was a problem hiding this comment.
p5) 현재 사용되지 않는 파일 같네요! 아마 클래스로 짜시다가 중간에 함수형으로 바꾸셨다고 했는데 그 과정에서 남기신게 아닐까.. 추측해봅니다!
src/App.js
Outdated
| console.log(window.location.pathname); | ||
| console.log(/^\/documents\//.test(location.pathname)); |
| @@ -0,0 +1,9 @@ | |||
| const debounce = (callback, delay) => { | |||
There was a problem hiding this comment.
debounce도 따로 빼서 만드신거 보고 감탄하고 갑니다..ㅎㅎ
metacode22
left a comment
There was a problem hiding this comment.
규란님 코드를 항상 보다보면 로직을 깔끔하게 처리할 뿐만 아니라 모듈화를 상당히 잘하시는 것 같아 정말 진심으로 배워가는 것이 많았습니다. 저도 황준일님이 작성하셨던 component 글을 봐서, 또 변수명과 코드 포맷팅을 잘 작성해주셔서 읽기 편했습니다!
그리고 5주간 ㅋㅋㅋ 저 때매 고생 많으셨습니다. ^ㅇ^ 코드 자주 훔쳐보러 오겠습니다.
| let inDebounce; | ||
| return (...args) => { | ||
| clearTimeout(inDebounce); | ||
| inDebounce = setTimeout(() => callback(...args), delay); |
There was a problem hiding this comment.
이렇게 arguments를 이용해서 쉽게 인자를 다음 함수에 전달할 수 있군요. 좋은 방법 알아갑니다!
| const addEvent = ($target, eventType, selector, callback) => { | ||
| const children = [...$target.querySelectorAll(selector)]; | ||
| const isTarget = (target) => children.includes(target) || target.closest(selector); | ||
|
|
||
| $target.addEventListener(eventType, (event) => { | ||
| if (!isTarget(event.target)) return false; | ||
| callback(event); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
저도 배워야 할 점 같습니다. 저는 addEventListener를 엄청 남발해뒀는데 이렇게 로직을 따로 빼두면 코드를 더 간결하게 만들 수 있겠네요. 한 번 써먹어 보겠습니다!
| this.render = () => { | ||
| this.$target.innerHTML = ` | ||
| <aside id="sidebar"></aside> | ||
| <main id="document-container"></main> |
There was a problem hiding this comment.
createElement()가 아니라 요런 식으로 요소를 만들어 볼 수 있겠군요!
| }, | ||
| }); | ||
| }; | ||
|
|
There was a problem hiding this comment.
요건 그냥 궁금한 건데요! sidebar.setState나 document.setState로 해당 컴포넌트를 재구성하는 것이 아니라, 위와 같이 계속해서 새로운 인스턴스를 만들어 해당 컴포넌트를 다시 그리셨던 이유가 궁금하네요!
| { path: /^\/documents\/new/, element: MainPage }, | ||
| { path: /^\/documents\//, element: MainPage }, | ||
| { path: /^\/404$/, element: NotFound }, | ||
| ]; |
There was a problem hiding this comment.
와하... 멋있다... 저는 간단하게 if문으로 구현했는데... 배워갑니다.. (메모... 📝)
src/Router.js
Outdated
|
|
||
| import { HISTORY_CHANGE_EVENT_NAME, routes } from "./constants/routes.js"; | ||
|
|
||
| export default function Router({ $target, onRoute }) { |
There was a problem hiding this comment.
혹시 Router 이렇게 생성자 함수를 통해 정의하신 이유가 따로 있나요??😀
src/components/document/Document.js
Outdated
| if (documentId === "new") { | ||
| renderNewDocument($header, $body); | ||
| } else if (!!documentId) { | ||
| console.log(documentId); |
| }; | ||
|
|
||
| this.setEvent = () => { | ||
| addEvent(this.$target, "keyup", "[name=title]", (event) => { |
There was a problem hiding this comment.
P5 : 저의 경우에는 event type을 keyup으로 해두면 화살표 키나 shift, caps lock 등 입력 값이 변하지 않았음에도 이벤트가 발생해서 유효하지 않은 api가 날라가더라구요. 그래서 input으로 바꿔서 입력값이 변할 때에만 api가 날라갈 수 있게 했었습니다.
그리고 이전에 React에서 겪은 적이 있는데, event type을 keyup으로 해두고 한글을 입력하면 이벤트가 중복해서 2번 발생하는 현상이 있었습니다. 실제로 검색해보니 javascript 자체의 문제더라구요. 한글 관련 에러여서 공식 자료도 찾기 어려웠었습니다. 이런 현상을 피하기 위해서 다른 event type을 고려해보시는 것도 좋을 것 같습니다!
p.s 과제에서 확인해보니, 여기선 keyup으로 해도 이벤트가 중복 발생하진 않네요!
src/components/document/Document.js
Outdated
| import API from "../../utils/api.js"; | ||
| import { USER } from "../../config.js"; | ||
| import { debounce } from "../../utils/debounce.js"; | ||
| import { getItemFromStorage, setItemToStorage } from "../../utils/storage.js"; |
src/App.js
Outdated
|
|
||
| this.route = () => { | ||
| const findMatchedRoute = () => routes.find((route) => route.path.test(location.pathname)); | ||
| const Page = findMatchedRoute()?.element || NotFound; |
There was a problem hiding this comment.
제가 잘 못 찾는 것 같습니다! 혹시 Page가 어디서 어떻게 사용되는지 알 수 있을까요? element를 routes에서 찾고 나서 렌더링하려면 new Page()처럼 실행이 되어야 할 것 같은데 보이지 않아서요!
There was a problem hiding this comment.
ask) 제가 아직 자바스크립트에 능통하지 못합니다.. findMatchedRoute()?.element에서 바로 뒤에 .연산자(?)가 붙으면 객체가 반환되는 건가요?
dmswl98
left a comment
There was a problem hiding this comment.
규란님!
항상 새벽까지 과제하시느라 수고많으셨습니다.
역시 깔끔함...! 🤯 규란님 코드보고 감탄만 하다가 갑니다..
그리고 배포해주셔서 꼭 자랑해주세요...
규란님 코드에서는 컴포넌트에 내부에 존재하는 코드가 모두 메서드나 함수로 감싸져있네요..?
전역으로 존재하는 변수가 없어서 컴포넌트 내부의 코드 실행 과정이 한 눈에 보여 더 좋은 것 같아요!
마지막 코드 리뷰라 많이 아쉽습니다...
5주 동안 수고하셨고 같이 공부할 수 있어서 너무나 좋았습니다! 🥰
내년 1월에 뵈어요!
src/Router.js
Outdated
|
|
||
| import { HISTORY_CHANGE_EVENT_NAME, routes } from "./constants/routes.js"; | ||
|
|
||
| export default function Router({ $target, onRoute }) { |
| const fetchDocument = async (documentId) => { | ||
| const response = await API.getDocuments(documentId); |
There was a problem hiding this comment.
API를 객체로 선언하시다니...! 이렇게 작성하면 실수를 줄일 수 있겠네요!😲
| this.setEvent = () => { | ||
| addEvent(this.$target, "keyup", "[name=content]", (event) => { | ||
| onEdit(event.target.innerText, "content"); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
이벤트 리스너를 따로 함수로 만드신 건가요?! 이렇게 코드를 작성할 수도 있군요... 😮
| { path: /^\/documents\/new/, element: MainPage }, | ||
| { path: /^\/documents\//, element: MainPage }, | ||
| { path: /^\/404$/, element: NotFound }, | ||
| ]; |
There was a problem hiding this comment.
와하... 멋있다... 저는 간단하게 if문으로 구현했는데... 배워갑니다.. (메모... 📝)
src/App.js
Outdated
|
|
||
| this.route = () => { | ||
| const findMatchedRoute = () => routes.find((route) => route.path.test(location.pathname)); | ||
| const Page = findMatchedRoute()?.element || NotFound; |
There was a problem hiding this comment.
ask) 제가 아직 자바스크립트에 능통하지 못합니다.. findMatchedRoute()?.element에서 바로 뒤에 .연산자(?)가 붙으면 객체가 반환되는 건가요?
| this.render = () => { | ||
| this.$target.innerHTML = ` | ||
| <aside id="sidebar"></aside> | ||
| <main id="document-container"></main> |
There was a problem hiding this comment.
createElement()가 아니라 요런 식으로 요소를 만들어 볼 수 있겠군요!
src/components/sidebar/Details.js
Outdated
| }); | ||
|
|
||
| documents.forEach(({ id, title, documents }) => { | ||
| const $li = this.$target.querySelector(`[data-document-id="${id}"]`); |
There was a problem hiding this comment.
p5) SidebarBody에서는 findDocumentElement()로 따로 메서드를 만들었는데, 여기에도 적용해 볼 수 있을까요? 일종의 통일성을 위해서요
src/components/sidebar/Sidebar.js
Outdated
| title: "Private", | ||
| rootDocuments: [...this.state.rootDocuments], | ||
| }, | ||
| onAddButtonClick: addDocument.bind(this), |
There was a problem hiding this comment.
ask) 프로젝트 전반에 걸쳐 bind()가 사용되는 것이 보입니다. addDocument의 this가 생성된 Sidebar를 가리키고 있지 않을가 생각이 드는데, bind()를 사용함으로 얻어지는 효과에 대해서 알 수 있을까요?
eastroots92
left a comment
There was a problem hiding this comment.
11월 26일 12시에 줌을 통해 구두로 피드백을 드렸고
기록 남기기용으로 구두로 이야기 나눴던 코드리뷰를 간략하게 적어두려 합니다.
CSS 관련
-
Reset CSS와 normalize CSS
가능하다면 의도적으로 넣어둘 것
JS 관련
-
클로저 패턴 + 관심사 별로 묶기
[vanilla-js-notion/api.js at feature · gxxrxn/vanilla-js-notion](https://github.com/gxxrxn/vanilla-js-notion/blob/feature/src/utils/api.js)
[vanilla-js-notion/storage.js at feature · gxxrxn/vanilla-js-notion](https://github.com/gxxrxn/vanilla-js-notion/blob/feature/src/utils/storage.js)
-
CustomEvent / 옵저버 패턴 / 펍섭패턴
기술 구조 관련
-
폴더 구조 잡기 (도메인 지역성을 고려한 구조, 기능 별로 모아두는 구조)
-
API
- ky, axios 등을 많이 사용함.
-
역할을 명확하게 하기
-
상수
- 전역에서 알아야 하는건가? 딱 그 곳에서만 알면 좋은 건가?
-
컴포넌트
- customElements ⇒ 커스텀한 컴포넌트를 만들 수 있다.
- container-component
const template = document.createElement('template'); template.innerHTML = ` <style> h3{ color:orange; } </style> <div class="user-card"> <h3></h3> </div> ` class UserCard extends HTMLElement{ constructor(){ super(); //shadow DOM this.attachShadow({mode:'open'}); this.shadowRoot.appendChild(template.content.cloneNode(true)); this.shadowRoot.querySelector('h3').innerText = this.getAttribute('name') // this.innerHTML = `<h3>${this.getAttribute('name')}</h3>` } } window.customElements.define('user-card', UserCard) <body> <user-card>안녕</user-card> </body>
-
기능(function)과 화면(presentation)
- api 에서 try catch 등을 통해 alert를 띄우는건 좋지않음.
import { push } from '../router.js'; import { API_END_POINT } from '../url.js'; import { USER_NAME } from './constants.js'; export const request = async (url, options = {}, data) => { const res = await fetch(`${API_END_POINT}${url[0] === '/' ? url : `/${url}`}`, { ...options, headers: { 'x-username': USER_NAME, 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (res.ok) { return await res.json(); } else { push('/'); throw new Error(`${res.status} Error`); } }; const getData({params}, callback);
-
-
Router 처리
- 역할 위임 (queryParams, url path)
- 관리하는법
const [, , documentId] = pathname.split("/"); // 이렇게 하면 URL 구조를 이해하기 쉽지 않음. 추가로 URL이 어려움.
HTML 관련
- 시맨틱 태그
기타
-
eof 이슈
editorConfig 등을 사용해서 지켜줄 것
-
XSS 이슈
📌 과제 설명
Vanilla JS로 노션을 클로닝합니다.
실행방법
.evn.js파일을 생성 후,API_END_POINT,USER_NAME을 작성해서 export 해주세요.npm serve -s으로 프로젝트를 실행시킬 수 있습니다.👩💻 요구 사항과 구현 내용
기본 요구사항
✅ PR 포인트 & 궁금한 점