1. 컴포넌트 생성 클래스
코어 Component 클래스 정의
먼저, 코어 Component
클래스를 정의하기 위해 payload
인수를 받아 tagName
과 같은 속성을 지정하여 동적으로 HTML 요소를 생성할 수 있게 해준다.
export class Component {
constructor(payload = {}) {
const { tagName = 'div' } = payload;
this.el = document.createElement(tagName);
this.render();
}
render() {}
}
- 생성자: 생성자는
payload
객체를 인수로 받아, 구조 분해 할당을 통해tagName
을 추출하고 기본값은'div'
로 설정한다. - 요소 생성:
this.el
은 지정된tagName
을 사용하여 새로 생성된 요소로 설정된다. - 렌더 메서드:
render
메서드가 호출되고 하위 클래스에서 특정 렌더링 로직을 정의하기 위해 재정의될 수 있도록 설계한다.
App 컴포넌트 확장
코어 Component
클래스를 확장하여 커스텀 컴포넌트를 만든다.
하위 클래스는 자체 tagName
을 지정하고 커스텀 렌더링 로직을 정의한다.
import { Component } from './core/heropy';
export default class App extends Component {
constructor() {
super({ tagName: 'h1' });
}
render() {
this.el.textContent = 'Hello World!';
}
}
- 생성자:
App
클래스는Component
클래스를 확장하고, 생성자는super()
를 호출하여tagName: 'h1'
을 지정하는 객체를 전달하여<h1>
요소를 생성한다. - 렌더 메서드:
render
메서드를 재정의하여 요소의textContent
를 "Hello World!"로 설정한다.
전체 코드 통합
const app = new App();
document.body.append(app.el);
- 인스턴스화:
App
의 인스턴스를 생성한다. - DOM 조작: "Hello World!" 텍스트가 포함된
<h1>
요소를 문서의 본문에 추가한다.
2. 선언적 렌더링과 이벤트 핸들링
입력(input) 요소와 버튼(button) 요소를 만들어, 버튼을 클릭하면 입력 값이 콘솔에 출력되는 코드를 작성한다.
이를 위해 선언적 렌더링과 이벤트 핸들링을 구현해본다.
코어 Component state 설정
export class Component {
constructor(payload = {}) {
const { tagName = 'div', state = {} } = payload;
this.el = document.createElement(tagName);
this.state = state;
this.render();
}
render() {}
}
먼저, payload
객체를 받아 tagName
과 state
를 설정한다. state
는 기본값으로 빈 객체 {}
를 갖도록 한다.
App 컴포넌트 확장
input
요소와 button
요소를 생성하고, 입력 이벤트와 클릭 이벤트를 처리하도록 한다.
import { Component } from './core/heropy';
export default class App extends Component {
constructor() {
super({ state: { inputText: '' } });
}
render() {
this.el.classList.add('search');
this.el.innerHTML = `
<input />
<button>Click!</button>
`;
const inputEl = this.el.querySelector('input');
inputEl.addEventListener('input', () => {
this.state.inputText = inputEl.value;
});
const buttonEl = this.el.querySelector('button');
buttonEl.addEventListener('click', () => {
console.log(this.state.inputText);
});
}
}
- 생성자: 생성자에서
state
를{ inputText: '' }
로 초기화한다. - 렌더 메서드:
render
메서드를 재정의하여 요소를 렌더링하고, 클래스 이름을 추가한다. - HTML 설정:
this.el.innerHTML
을 통해input
과button
요소를 생성한다. - 이벤트 핸들링:
input
요소에input
이벤트 리스너를 추가하여 입력 값을this.state.inputText
에 저장한다.button
요소에click
이벤트 리스너를 추가하여 현재this.state.inputText
값을 콘솔에 출력한다.
3. 조건과 반복을 활용한 컴포넌트 렌더링
App 컴포넌트 확장
상태로 갖는 과일 목록을 가격이 3000 미만인 과일들만 렌더링하도록 한다.
import { Component } from './core/heropy';
export default class App extends Component {
constructor() {
super({
state: {
fruits: [
{ name: 'Apple', price: 1000 },
{ name: 'Banana', price: 2000 },
{ name: 'Cherry', price: 3000 },
],
},
});
}
render() {
this.el.innerHTML = `
<h1>Fruits</h1>
<ul>
${this.state.fruits
.filter(fruit => fruit.price < 3000)
.map(fruit => `<li>${fruit.name}</li>`)
.join('')}
</ul>
`;
}
}
- 생성자:
state
에 과일 목록을 설정한다. - 렌더 메서드:
render
메서드를 재정의하여 과일 목록을 렌더링한다.- 필터링:
this.state.fruits
배열을filter
메서드를 사용하여 가격이 3000 미만인 과일들만 추출한다. - 매핑:
map
메서드를 사용하여 각 과일 객체를<li>
요소로 변환한다. - 문자열 병합:
join('')
을 사용하여 배열을 하나의 문자열로 결합한다.
- 필터링:
- HTML 설정: 필터링되고 매핑된 과일 목록을 포함하는 HTML 구조를
this.el.innerHTML
에 설정한다.
4. 자식 컴포넌트에게 데이터 전달
코어 Component props 설정
export class Component {
constructor(payload = {}) {
const { tagName = 'div', state = {}, props = {} } = payload;
this.el = document.createElement(tagName);
this.state = state;
this.props = props;
this.render();
}
render() {}
}
먼저, payload
객체를 받아 props
를 설정한다. props
는 기본값으로 빈 객체 {}
를 갖도록 한다.
App 컴포넌트 확장
상태로 갖는 과일 목록을 가지고, 각 과일 항목을 자식 자식 컴포넌트인 FruitItem
에게 전달한다.
import { Component } from './core/heropy';
import FruitItem from './components/FruitItem';
export default class App extends Component {
constructor() {
super({
state: {
fruits: [
{ name: 'Apple', price: 1000 },
{ name: 'Banana', price: 2000 },
{ name: 'Cherry', price: 3000 },
],
},
});
}
render() {
this.el.innerHTML = `
<h1>Fruits</h1>
<ul></ul>
`;
const ulEl = this.el.querySelector('ul');
ulEl.append(
...this.state.fruits.map(
fruit =>
new FruitItem({
props: {
name: fruit.name,
price: fruit.price,
},
}).el,
),
);
}
}
- 자식 컴포넌트 생성:
this.state.fruits
배열을 순회하며 각 과일 항목에 대해FruitItem
인스턴스를 생성하고, 이를<ul>
요소에 추가한다.
자식 컴포넌트(FruitItem) 구현
FruitItem
컴포넌트는 Component
클래스를 상속받아 각 과일 항목의 정보를 표시한다.
jsx코드 복사
import { Component } from '../core/heropy';
export default class FruitItem extends Component {
constructor(payload) {
super({ tagName: 'li', props: payload.props });
}
render() {
this.el.innerHTML = `
<span>${this.props.name}</span>
<span>${this.props.price}</span>
`;
this.el.addEventListener('click', () => {
console.log(this.props.name, this.props.price);
});
}
}
- 생성자:
FruitItem
클래스는Component
클래스를 상속하며,tagName
을'li'
로 설정하고props
를 전달받는다. - 렌더 메서드:
render
메서드를 재정의하여 과일 이름과 가격을 표시한다. - 이벤트 핸들링: 각 과일 항목을 클릭하면 해당 과일의 이름과 가격을 콘솔에 출력한다.
5. 해쉬 라우터 관리
SPA(Single Page Application) 구현을 위해, URL 해쉬를 기반으로 페이지를 라우팅하는 방법이다.
이를 통해 전체 페이지를 다시 로드하지 않고도 URL 변경에 따라 다른 컴포넌트를 렌더링할 수 있다.
routeRender
함수
해쉬 변경 시 적절한 컴포넌트를 찾아 렌더링할 수 있도록 하는 routeRender
함수를 작성한다.
function routeRender(routes) {
// URL에 해쉬가 없는 경우 기본 해쉬 설정
if (!location.hash) {
history.replaceState(null, '', '/#/');
}
const routerView = document.querySelector('router-view');
// 해쉬와 쿼리 문자열 분리
const [hash, queryString = ''] = location.hash.split('?');
// 쿼리 문자열을 객체로 변환
const query = queryString.split('&').reduce((acc, cur) => {
const [key, value] = cur.split('=');
acc[key] = value;
return acc;
}, {});
// 쿼리 객체를 히스토리 상태로 설정
history.replaceState(query, '', '');
// 현재 해쉬에 맞는 라우트 찾기
const currentRoute = routes.find(route => new RegExp(`${route.path}/?$`).test(hash));
// router-view 요소 비우고 새 컴포넌트 렌더링
routerView.innerHTML = ``;
routerView.append(new currentRoute.component().el);
// 스크롤을 최상단으로 이동
window.scrollTo(0, 0);
}
- 기본 해쉬 설정: 페이지가 처음 로드될 때 해쉬가 없으면 기본 해쉬를
/#/
로 설정한다. - router-view 요소 선택: 라우터가 컴포넌트를 렌더링할
router-view
요소를 선택한다. - 해쉬와 쿼리 문자열 분리:
location.hash
를 사용하여 해쉬와 쿼리 문자열을 분리한다. - 쿼리 문자열 파싱:
- 쿼리 문자열을
&
로 분리하고, 각 쿼리 파라미터를=
로 분리하여 객체 형태로 변환한다. - 이 쿼리 객체를
history.replaceState
를 통해 히스토리 상태로 설정한다.
- 쿼리 문자열을
- 현재 라우트 찾기: 라우트 배열에서 현재 해쉬에 맞는 라우트를 정규식을 사용하여 찾는다.
- 컴포넌트 렌더링:
router-view
요소를 비우고, 현재 라우트에 맞는 컴포넌트를 생성하여 추가한다.- 화면을 최상단으로 스크롤되도록 한다.
createRouter
함수
라우터를 초기화하고, 해쉬 변경 시 routeRender
함수를 호출하도록 이벤트 리스너를 설정한다.
export function createRouter(routes) {
return function () {
window.addEventListener('popstate', () => {
routeRender(routes);
});
routeRender(routes);
};
}
- popstate 이벤트 리스너 추가: 브라우저의 뒤로 가기/앞으로 가기 버튼 클릭 시
popstate
이벤트가 발생한다. 이 때routeRender
함수를 호출하여 현재 상태에 맞는 컴포넌트를 렌더링한다. - 초기 렌더링: 페이지 로드 시
routeRender
함수를 호출하여 초기 상태에 맞는 컴포넌트를 렌더링한다.
routeRender
와 createRouter
함수 사용
import TheHeader from './components/TheHeader';
import { Component } from './core/heropy';
export default class App extends Component {
render() {
const routerView = document.createElement('router-view');
this.el.append(new TheHeader().el, routerView);
}
}
import App from './App';
import router from './routes';
const root = document.querySelector('#root');
root.append(new App().el);
router();
App
컴포넌트를 루트 요소에 추가하고, 라우터를 초기화하여 페이지 로드 시 라우터가 동작하도록 설정한다.
컴포넌트 예제
Home
과 About
컴포넌트를 정의하여 각각의 페이지에서 렌더링되도록 한다.
import { Component } from '../core/heropy';
export default class Home extends Component {
render() {
this.el.innerHTML = `<h1>Home</h1>`;
}
}
import { Component } from '../core/heropy';
export default class About extends Component {
render() {
const { a, b, c } = history.state;
this.el.innerHTML = `
<h1>About</h1>
<h2>${a}</h2>
<h2>${b}</h2>
<h2>${c}</h2>
`;
}
}
6. 상태 관리
Store
클래스를 사용하여 상태를 관리하고, 컴포넌트가 해당 상태를 구독(subscribe)하여 상태 변화에 따라 자동으로 업데이트되도록한다.
코어 Store 클래스 정의
Store
클래스는 상태(state)를 관리하고, 상태가 변경될 때 이를 구독하여 컴포넌트에 알리는 역할을 한다.
export class Store {
constructor(state) {
this.state = {};
this.observers = {};
for (const key in state) {
Object.defineProperty(this.state, key, {
get: () => state[key],
set: val => {
state[key] = val;
if (this.observers[key]) {
this.observers[key].forEach(observer => observer(val));
}
},
});
}
}
subscribe(key, cb) {
if (Array.isArray(this.observers[key])) {
this.observers[key].push(cb);
} else {
this.observers[key] = [cb];
}
}
}
- 상태 초기화:
constructor
는 초기 상태를 받아 이를this.state
에 저장하고, 각 상태 값에 대해getter
와setter
를 정의한다. - 상태 변경 감지:
setter
는 상태가 변경될 때마다 해당 상태를 구독하는 모든 콜백 함수를 호출한다. - 구독 관리:
subscribe
메서드는 특정 상태 키를 구독하는 콜백 함수를 등록한다.
상태를 구독하는 컴포넌트
Message
컴포넌트는 Store
의 상태를 구독하고, 상태가 변경될 때마다 자동으로 다시 렌더링된다.
import { Component } from '../core/heropy';
import messageStore from '../store/message';
export default class Message extends Component {
constructor() {
super();
messageStore.subscribe('message', () => {
this.render();
});
}
render() {
this.el.innerHTML = `
<h2>${messageStore.state.message}</h2>
`;
}
}
- 상태 구독:
constructor
에서messageStore
의message
상태를 구독하여 상태가 변경될 때마다render
메서드를 호출한다. - 렌더링:
render
메서드는 현재 상태 값을 사용하여 컴포넌트의 내용을 업데이트한다.
Store 인스턴스 생성
Store
클래스를 이용하여 애플리케이션에서 사용할 상태를 관리하는 messageStore
인스턴스를 생성합니다.
import { Store } from '../core/heropy';
export default new Store({ message: 'Hello~' });
- 초기 상태 설정:
message
라는 초기 상태 값을 설정하여messageStore
인스턴스를 생성한다.
'🍪 카카오 테크 캠퍼스 2기' 카테고리의 다른 글
[카카오 테크 캠퍼스 2기] 1단계 9주차 WIL - TypeScript (0) | 2024.06.24 |
---|---|
[카카오 테크 캠퍼스 2기] 1단계 8주차 WIL - API를 활용한 영화 검색 사이트 (0) | 2024.06.24 |
[카카오 테크 캠퍼스 2기] 1단계 5 · 6주차 WIL - JavaScript 심화 (0) | 2024.05.27 |
[카카오 테크 캠퍼스 2기] 웰컴키트 굿즈 개봉기 (0) | 2024.05.14 |
[카카오 테크 캠퍼스 2기] 1단계 4주차 WIL - SCSS (0) | 2024.05.06 |