고양이 사진첩 만들기 - (1) Nodes, Breadcrumb
폴더 구조
구현 시 유의사항
- UI요소는 가급적 컴포넌트 형태로 추상화 하여 동작
- 각 컴포넌트가 서로 의존성을 지니지 않고, App 혹은 그에 준하는 컴포넌트가 조율하는 형태로 동작하도록 지향
- API 호출 중 에러 발생에 대한 처리
- 의존성이 느슨한 구조로 작성
- 오류 발생 시 체크
- 사용자에게 오류발생 인지 시키기
- ES6 모듈 형태
- API 호출은 가급적 fetch 함수 사용
- async, await 문 사용
- fetch 외의 방법에서는, 동기 호출 방식 x
- API 처리 코드 별도로 분리
- 알아보기 쉬운 네이밍, 일관된 코드 포맷팅 규칙 유지하고, 코드 중복은 지양
- 전역 오염 최소화
- 이벤트 바인딩은 최적화해서 사용
구현
index.js
import App from './App.js';
new App(document.querySelector('.App'));
index.html
<html>
<head>
<title>고양이 사진첩!</title>
<link rel="stylesheet" href="./src/styles/style.css">
<!-- type="module"을 추가함 -->
<script src="./src/index.js" type="module"></script>
</head>
<body>
<h1>고양이 사진첩</h1>
<main class="App">
</main>
</body>
</html>
es6의 import, export 기능을 이용하면 index.html에서 모듈 의존 순서에 맞게 script src로 스크립트를 불러올 필요 없이, index.js만 불러오게 하고 나머지는 각 컴포넌트에서 필요한 스크립트만 import해서 쓸 수 있습니다.
module
프론트엔드의 규모가 커짐에 따라 js코드를 여러 파일과 폴더에 나누어 작성하고 서로를 효율적으로 불러올 수 있도록 하기위해 모듈 시스템이 ES2015에 추가되었습니다.
- import 혹은 export 구문을 사용할 수 있습니다.
- 엄격 모드(strict mode)로 동작합니다.
- 모듈의 가장 바깥쪽에서 선언된 이름은 전역 스코프가 아니라 모듈 스코프에서 선언됩니다.
- 같은 모듈을 다른 모듈에서 여러 번 불러도, 모듈 내부 코드는 처음 단 한번만 실행됩니다.
import, export
모듈을 내보내고 불러오는 방법
- 가독성이 좋다.
- 비동기로 작동하고 실제로 쓰이는 부분만 불러올 수 있기 때문에 성능 및 메모리 측면에서 유리하다.
- es6를 지원하지 않는 브라우저에서는 babel 또는 CommonJS의 require 방식을 사용해야한다.
querySelector
선택자 또는 선택자 뭉치와 일치하는 문서 내 첫 번째 Element를 반환
https://developer.mozilla.org/ko/docs/Web/API/Document/querySelector
Nodes.js
export default function Nodes({ $app, initialState, onClick, onBackClick }) {
this.state = initialState;
this.$target = document.createElement('ul');
$app.appendChild(this.$target);
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
this.onClick = onClick;
this.onBackClick = onBackClick;
this.render = () => {
if (this.state.nodes) {
const nodesTemplate = this.state.nodes
.map((node) => {
const iconPath =
node.type === 'FILE'
? './assets/file.png'
: './assets/directory.png';
return `
<div class="Node" data-node-id="${node.id}">
<img src="${iconPath}" />
<div>${node.name}</div>
</div>
`;
})
.join('');
this.$target.innerHTML = !this.state.isRoot
? `<div class="Node"><img src="/assets/prev.png"></div>${nodesTemplate}`
: nodesTemplate;
}
this.$target.querySelectorAll('.Node').forEach(($node) => {
$node.addEventListener('click', (e) => {
const { nodeId } = e.target.dataset;
if (!nodeId) {
this.onBackClick();
}
const selectedNode = this.state.nodes.find(
(node) => node.id === nodeId
);
if (selectedNode) {
this.onClick(selectedNode);
}
});
});
};
this.render();
}
생성된 DOM을 어디에 append할지를 $app 파라미터로 받기, 파라미터는 구조 분해 할당 방식으로 처리
Nodes 컴포넌트를 렌더링 할 DOM을 this.$target이라는 이름으로 생성
this.setState는 state를 받아서 현재 컴포넌트의 state를 변경하고 다시 렌더링합니다.
this.render는 현재 상태를 기준으로 렌더링합니다.
클릭 가능한 모든 요소에 click 이벤트를 걸어줍니다.
dataset을 통해 data-로 시작하는 attribute를 꺼내올 수 있습니다.
this.render()를 통해 new로 생성하자마자 렌더링되도록 합니다.
onClick은 Breadcrumb 처리를 하기 위한 것으로 상위 컴포넌트인 App에서 정의하고 Nodes에서 파라미터로 받아 사용합니다. 이런 식으로 작성하여 Nodes컴포넌트에서는 Breadcrumb에 관한 코드를 작성하지 않아도 됩니다.
Breadcrumb.js
export default function Breadcrumb({ $app, initialState }) {
this.state = initialState;
this.$target = document.createElement('nav');
this.$target.className = 'Breadcrumb';
$app.appendChild(this.$target);
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
this.render = () => {
this.$target.innerHTML = `
<div class="nav-item">root</div>
${this.state
.map(
(node, index) => `
<div class="nav-item" data-index="${index}">${node.name}</div>
`
)
.join('')}
`;
};
}
Breadcrumb를 렌더링 할 DOM this.$target 생성
컴포넌트간의 의존도 줄이기
여기서 Nodes의 요소에 따라 클릭 시 동작이 달라지게 됩니다.
해당 Node가 File인 경우에는 File 이미지 보기, Node가 Directory인 경우 해당 Directory로 이동하고 Breadcrumb또한 갱신이 되어야 합니다.
즉, Nodes에 일어나는 인터랙션에 의해 Breadcrumb에도 영향을 주어야 합니다.
이때, Nodes 코드 내에서 Breadcrumb를 직접 다루게되면 Breadcrumb에 의존성이 생기기 때문에 Nodes만 필요한 화면에서는 사용하기 힘들게 됩니다.
이런 경우 일반적으로 두 컴포넌트의 상위 컴포넌트에서 콜백 함수를 통해 조율하는 방법을 지향합니다.
출처