개요
사진첩 사이트를 구현했다.
앱 기동시 root 디렉토리의 폴더,파일들이 렌더링되고 breadcrumb에 경로가 표시된다.
폴더를 클릭해서 해당 폴더를 볼 수 있고, 파일을 클릭시 이미지를 볼 수 있다.
이미지창은 ESC 혹은 이미지 밖 클릭 시 닫을 수 있다.
breadcrumb의 경로를 클릭 시 해당 경로로 이동한다.
데이터 로딩 시에 로딩 UI가 보여지고 로딩이 끝나면 없어진다.
추가 요구사항
- UI 요소는 컴포넌트화, 각 컴포넌트가 의존성을 지니지 말 것
- API 호출 중 에러가 발생했을 때의 처리 , 사용자에게 인지시킬 것
- ES6 모듈 형태로 작성
- API 호출은 fetch로 , async await
- 가독성 , 중복 지양
- 이벤트 바인딩은 최적화
- 한번 로딩된 데이터는 메모리에 캐시하여 다시 탐색할 경우 캐시된 데이터를 불러와 렌더링
흐름
앱 첫 기동시 데이터를 받아오고, 해당 데이터를 활용해 breadCrumb(경로)와 nodes(디렉토리,파일) UI를 렌더링 한다.
디렉토리 변경(디렉토리 클릭 시 발생)시 custom Event를 활용해서 window.dispatchEvent(new CustomEvent(...)) 이렇게 이벤트를 발생시키고,
해당 이벤트에 이벤트 리스너를 추가해서 컴포넌트간 의존성 없이 각 컴포넌트들을 다시 업데이트 하게 했다.
파일 클릭 시 이미지 모달 UI를 렌더링하고 이미지 밖,ESC 키 누를 시 해당 UI를 없앴다.
로딩 중에는 이미지 로딩 UI를 렌더링하고, 로딩이 끝나면 없앴다.
데이터 로딩
async function request(url = '') {
const BASE_URL = "https://zl3m4qq0l9.execute-api.ap-northeast-2.amazonaws.com/dev"
try {
//로딩창 띄우기 - 생략
const response = await fetch(BASE_URL+url)
//로딩창 없애기 - 생략
if(response.ok) {
const json = await response.json()
return json
}
throw Error("통신 실패")
} catch(e) {
console.error(e.message)
// 사용자한테 오류 났음 알리기 - 생략
}
}
async await 사용해서 구현
fetch 해서 받아온 값을 확인해서 통신이 잘 됬으면 response.json()을 넘긴다
에러 발생시 콘솔창에 에러 메세지를 출력한다. 또 사용자에게 에러가 발생했음을 알린다.
데이터 렌더링
받아온 데이터를 각 컴포넌트에 전달해 UI를 렌더링했다. 크게 Breadcrumb(경로)와 Nodes(폴더,파일들)로 나눴다.
Breadcrumb.js
import { directoryChange } from "../utils.js";
function Breadcrumb() {
this.paths = []
const breadcrumb = document.querySelector(".Breadcrumb")
this.setPaths = (nextPaths) => {
this.paths = nextPaths
this.render()
}
this.movePath = (path) => {
let after = JSON.parse(JSON.stringify(this.paths))
for(let i = 0; i < this.paths.length ; i++) {
if(path.id === this.paths[i].id) {
after = after.slice(0,i+1)
this.setPaths(after)
return
}
}
this.setPaths([...after,path])
}
this.render = () => {
breadcrumb.innerHTML = `${this.paths.map((item) =>`<div data-id=${item.id}>
${item.name}
</div>`).join(' ')}`
}
const handleClick = (event) => {
const pathId = parseInt(event.target.dataset.id)
const pathName = event.target.textContent.trim()
if(pathId !== null && pathId !== this.paths[this.paths.length-1].id) {
// 현재 보고 있는 화면이 아니면 이동
directoryChange(pathId, pathName)
}
}
breadcrumb.addEventListener('click', handleClick)
}
const breadcrumb = new Breadcrumb()
export { breadcrumb }
경로를 보여주는 애다.
1) paths에 경로들을 상태로 가지고 있게 했다.
2) setPaths로 경로들을 업데이트하고, 그에 맞게 화면이 다시 렌더링하게 했다.
3) movePath는 현 디렉토리에서 다른 디렉토리로 이동하는 함수다. 크게 2가지의 경우가 있다.
A. 파일,폴더 목록(nodes)에서 폴더를 클릭해서 해당 디렉토리로 이동하는 경우
B. breadCrumb의 특정 경로를 클릭해서 해당 디렉토리로 이동하는 경우
2가지 경우를 모두 처리하기 위해서
먼저 기존 paths를 쭉 봐서 가려고하는 path가 있는지 확인한다.
있으면 (B) paths 배열을 걔까지 slice로 짤라서 짜른 배열을 setPaths에 전달해 상태를 업데이트했다.
없으면 (A) 기존 paths에 가려고하는 애를 추가해서 상태를 업데이트했다.
- render는 UI를 그리는 함수이다. paths를 활용해서 그린다.
- 각 path 클릭시 디렉토리를 이동해야 된다. 그걸 처리하기 위해서 이벤트를 걸어줬다. 클릭 시에 path의 Id,Name을 갖고와서 디렉토리 이동 시켜주는 함수 directoryChange에 전달한다.
function directoryChange(nodeId, nodeName) {
// 새 디렉토리에 맞는 정보 가져와서
// 화면 다시 렌더링
window.dispatchEvent(new CustomEvent("DIRECTORY_CHANGE", {
detail: {nodeId, nodeName}
}))
}
directoryChange는 커스텀 이벤트를 발생시킨다.
Nodes.js
import { directoryChange, directoryPrev, openFile } from "../utils.js";
function Nodes() {
// 생략
this.setNodes = (nextNodes) => {
// 생략
}
this.render = () => {
// 생략
}
const handleClick = (event) => {
const node = event.target.closest(".Node")
if(node) {
if(node.dataset.type === "DIRECTORY") {
// 디렉토리 이동
directoryChange(parseInt(node.dataset.id), node.dataset.name)
} else if(node.dataset.type === "FILE") {
// 이미지 열기
const filePath = node.dataset.filePath
openFile(filePath)
} else if(node.dataset.type === "PREV") {
directoryPrev()
}
}
}
domNodes.addEventListener('click', handleClick)
// this.render()
}
const nodes = new Nodes()
export { nodes }
얘는 파일,폴더 목록을 보여주는 애다
상태변경, 렌더링은 위에 거랑 똑같은 로직이다. 클릭시 파일이면 이미지 모달을 띄우고 폴더면 디렉토리이동을 시켰다.
파일이면 경로를 받은다음에 openFile 함수에 전달했다.
function openFile(filePath) {
new ModalImageViewer(filePath).render()
}
오픈파일 함수는 모달을 렌더링한다.
해당 경로가 root가 아니면 뒤로가기 노드가 보여지게 했다.
nodes[0].parent !== null ? `
<div class='Node' data-type="PREV">
<img src='./assets/prev.png'>
</div> `: ""}`
그리고 얘를 클릭시 뒤 directory로 가는 directoryPrev 함수를 호출했다.
function directoryPrev() {
window.dispatchEvent(new CustomEvent("DIRECTORY_PREV"))
}
얘는 커스텀이벤트를 발생시킨다.
ModalImageViewer.js
export default function ModalImageViewer(filePath) {
// 생략
this.render = () => {
// 생략
}
this.remove = () => {
App.removeChild(modalImageViewer)
document.removeEventListener('keydown', handleKeyDown)
}
const handleClick = (event) => {
if(!event.target.closest("img")) {
this.remove()
}
}
const handleKeyDown = (event) => {
if(event.keyCode === 27) {
this.remove()
}
}
modalImageViewer.addEventListener('click', handleClick)
document.addEventListener('keydown', handleKeyDown)
}
얘는 이미지 클릭시 해당 이미지를 보여주는 애다.
remove 메소드로 그려진애를 지우게 했다.
그다음 event.target.closet를 활용해서 img밖의 영역 클릭시 없어지게 했다. ESC 눌렀을 때도 없어지게 했다.
modalLoading.js
모달로딩은 설명할게 없다. 걍 gif가 뜨게 했다.
디렉토리 이동
디렉토리 이동 시 커스텀이벤트를 발생시켰다. 이 커스텀 이벤트에 이벤트 리스너를 바인딩해서 디렉토리 이동을 처리했다.
init.js
function init() {
// root의 정보를 불러와서 렌더링.
request().then((rootChilds) => {
breadcrumb.setPaths([{name: "root", id:0}])
nodes.setNodes(rootChilds)
})
// 디렉토리 변경 이벤트 핸들러 추가
const onDirectoryChange = (event) => {
const { nodeId, nodeName } = event.detail
const url = nodeId === 0 ? '' : `/${nodeId}`
const path = {name: nodeName, id:nodeId }
const modalLoading = new ModalLoading()
modalLoading.render()
request(url).then((childs) => {
// 만약 paths안에 name이 있으면 그 앞까지만 살린다.
// 없으면 맨 뒤에 추가
modalLoading.remove()
breadcrumb.movePath(path)
nodes.setNodes(childs)
})
}
const onDirectoryPrev = (event) => {
const prevNode = breadcrumb.paths[breadcrumb.paths.length-2]
const newEvent = {detail: { nodeId: prevNode.id, nodeName: prevNode.name }}
onDirectoryChange(newEvent)
}
window.addEventListener("DIRECTORY_PREV", onDirectoryPrev)
window.addEventListener("DIRECTORY_CHANGE", onDirectoryChange )
}
init()
디렉토리 변경시에는 전달받은 id,name을 활용해서 데이터를 받아오고 그 데이터를 바탕으로 화면을 다시 그렸다.
뒤로가기 시에는 breadcrumb의 paths에서 직전 노드의 정보를 받고 걔를 onDirectoryChange 에 전달해서 뒤로 가게 했다.
첫 기동시 root의 정보를 받아와서 화면을 그리게 했다.
리뷰
- 컴포넌트 의존성, 중복 없애기 -> 일단 각 컴포넌트에서 다른 컴포넌트를 import해오지 않게 했다. 대신에 utils.js와 index.js에서 각 컴포넌트를 통합적으로 관리하게 했다. 그리고 customEvent를 활용해서 각 컴포넌트에 디렉토리 변경을 알렸다. 공식 해설에 보면 이렇게 하는 대신에 통합 index.js에서 각 컴포넌트에 {onClick: () => {딴 컴포넌트 조작}} 이런 식으로 전달을 해줬던데, 어떤 방식이 더 나은지 좀 알아보고 저 방식도 써보는 식으로 바꿔봐야겠다.
- 이벤트 바인딩 최적화 -> 이벤트 위임을 사용해서 부모에 이벤트 리스너 걸고 target을 closest나 다른 것들로 판별해서 작동하게 했다. 잘했다.
- 데이터 캐싱 -> 얘는 따로 구현하지를 못했다. 전역쪽에 cache 객체 하나 만들어서 path ID 를 key로 value를 Nodes로 저장하는 방식으로 구현해 봐야 겠다.
- fetch와 async await 활용, 오류 처리 -> fetch는 catch가 지원이 안되서 response.ok를 활용해서 통신 성공여부를 판별하고 실패시 try catch 활용해 catch 절에서 처리했다. fetch 앞에 await 붙여서 async 함수 안에서 동기적으로 작동하게 했다. async는 response.json()을 리턴하는데, 얘를 프로미스로 감싸서 리턴한다. 그래서 얘를 가져다 쓸 떄 request().then(()=>할일) 요런식으로 풀어서 사용을 했다. 다르게 쓰는 방식은 없을지 한번 찾아보고 리팩토링 때 적용해 봐야 겠다.
- 가독성, 중복 지양 -> 함수 이름도 나름 잘 정하고 중복도 줄인 것 같다.
- 상태 변경에 따른 UI 업데이트 -> 이전 Vanila JS로 SPA만들기에서 제대로 못해서 여기서 잘 적용해서 만들었다. 상태 변경시 다시 render() 호출해서 UI를 업데이트 했다.
- 모듈형태로 작성 -> 했다.
'프론트엔드 > JavaScript, TypeScript' 카테고리의 다른 글
?. - Optional chaining 연산자 / ?? - Null 병합 연산자 (0) | 2022.08.22 |
---|---|
Promise 일정 시간 초과시 대기 취소하고 에러 처리하기 (1) | 2022.08.07 |
디바운스와 쓰로틀 (0) | 2022.07.24 |
Vanila Js로 SPA 만들기 - 리팩토링 (0) | 2022.03.15 |
Vanila Js로 SPA 만들기 (0) | 2022.03.10 |