자바스크립트 ES6+: 모듈(module) 기능을 이용하여 리액트의 hook 흉내내기 (reactify)
예전에 어느 프론트엔드 과제 테스트에서 아래와 같은 요구사항을 제시한 적이 있습니다.
요약하면 ‘순수 자바스크립트로 리액트와 유사한 구조의 웹 페이지를 만들어봐라’ 이런 뜻인데요, 준비가 되어 있지 않은 상태에서 이런 요구사항을 접하면 많이 난감할 것이라고 생각됩니다.
문제에서 컴포넌트를 제작할 때 function, class 둘 중 하나를 택하라 했는데 여기서는 function을 선택해서 간단한 숫자 카운터 예제를 만들어 보도록 하겠습니다. (참고로 실제 문제 풀이에서는 class기반으로 작성하는게 훨씬 편합니다.)
최신 브라우저에서는 스크립트 로딩 시 type="module" 옵션을 추가하면 별도의 웹팩 등이 없어도 모듈화를 진행할 수 있습니다.
여기에 있는 방법은 최선의 방법이 아니며, 더 좋은 방법을 알아내면 나중에 추가 보충하도록 하겠습니다.
작동 화면
1) 메인 html을 작성합니다. (index.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>숫자 카운터</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
<div id="app"></div>- 이 부분은 js를 통해 각종 컴포넌트를 추가할 때 타겟이 되는 부모 요소입니다.<script type="module" src="./main.js"></script>- 모듈import,export가 가능하도록 하여 스크립트 파일을 로딩합니다.
2) main.js 작성
1
2
3
4
5
import {render} from './util.js'
import App from './App.js'
render(document.querySelector("#app"), App())
render 함수는 컴포넌트 요소를 렌더링하는 함수로 밑에서 설명합니다. 아이디가 app인 요소를 가져와서 App.js 의 함수를 하위 자식으로 추가해 렌더링합니다.
3) util.js 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const stateArr = []
function render(parentElement, element) {
parentElement.appendChild(element)
}
const makeStateSetter = (wrapObj) => {
return (anotherValue) => {
wrapObj.value = anotherValue
}
}
function useState(initialValue) {
const wrapObj = {
value: initialValue,
}
stateArr.push(wrapObj)
return [wrapObj, makeStateSetter(wrapObj)]
}
export {render, useState}
render()- 상위 요소에 하위 요소를 추가하는 요소입니다. 추가 기능만 있으므로render라는 이름을 쓰기엔 부족하지만 일단은 이 이름을 사용하겠습니다. 원래 이 기능은 state 값이 변경될 때마다 자동으로 재 렌더링을 해야 합니다.stateArr배열 - 공통으로 사용하는 변수인state들의 객체를 배열 형태로 저장합니다.makeStateSetter익명함수 -state관리시 사용할setter함수를 만듭니다.useState- 리액트의useState와 유사한 기능을 수행하는 함수입니다.
4) App.js 작성
App 컴포넌트는 최상위 컴포넌트입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {render} from './util.js'
import HeadTitle from './HeadTitle.js'
import Content from './Content.js'
import Counter from './Counter.js'
export default function App() {
const $div = document.createElement('div')
render($div, HeadTitle())
render($div, Content())
render($div, Counter())
return $div
}
import문은 컴포넌트나 유틸리티 모듈을 로딩합니다.export default function App()- 이 함수를 기본적(default)으로 내보내겠다는 뜻입니다. 이 부분이 어떻게import되는지는main.js를 참고하세요.$div요소를 만들고,render과정을 거친 뒤 리턴합니다
5) HeadTitle.js , Content.js 작성
1
2
3
4
5
6
7
8
export default function HeadTitle() {
const $h2 = document.createElement('h2')
$h2.textContent = "카운터 예제"
return $h2
}
1
2
3
4
5
6
7
8
export default function Content() {
const $div = document.createElement('div')
$div.textContent = "사용법: [증가] 버튼을 누르면 숫자가 증가합니다."
return $div
}
이 두 파일은 단순히 HTML 요소를 만들어 코딩한 뒤 리턴하는 역할을 합니다.
6) Counter.js 작성
여기에는 숫자를 표시하는 부분과, 숫자를 증가시키는 버튼으로 나뉘어져 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {useState} from './util.js'
export default function Counter() {
const $div = document.createElement('div')
// 초기값 설정
const [count, setCount] = useState(0)
const $p = document.createElement('p')
$p.textContent = count.value
const $button = document.createElement("button")
$button.textContent = "증가"
$button.addEventListener('click', () => {
setCount(count.value + 1) // 카운터 값 증가
console.log(count)
$p.textContent = count.value // (!) 값이 바뀔 때 DOM에 직접 접근
})
$div.appendChild($p)
$div.appendChild($button)
return $div
}
const [count, setCount] = useState(0)count는 값이 저장되어 있는 객체 주소 변수,setCount는 이 객체의 값을 변경시키는 함수입니다. 비구조화 할당 기법을 이용해 변수 2개를 생성했습니다.- 참조 문제로 인해
count는 객체 자체이며 값은count.value로 접근합니다.
setCount(count.value + 1)- 카운터 값을 현재보다 1 증가시킵니다.$p.textContent = count.value- 상태값이 바뀌었을 때$p의 값을 직접 변경하도록 되어있습니다. 문제를 이렇게 풀면 DOM 접근 최소화를 하라는 피드백을 받을 것 같네요. 위에서도 언급했듯이state의 값이 바뀌는것을 감지해서 렌더링을 실행해야 하는데 이 부분은 더 공부하도록 하겠습니다.
This post is licensed under
CC BY 4.0
by the author.
