이 글을 읽기에 앞서
이 게시글은 리액트처럼 동작 하는 코드를 작성해보는 것이지 리액트와 100% 동일한 코드를 작성하려 하는 것이 아닙니다.
기존의 리렌더
기존의 리렌더링(사실 리렌더가 아니고 다른걸 렌더하는 거지만)은 명시적으로 다른 element를 넘겨서 업데이트 하는 형태였다.
import App from './components/App';
import React from './core/React';
import { Change } from './components/Change';
const app = document.getElementById('root');
const prev = App();
React.render(prev, app);
setTimeout(() => React.updateDOM(app, prev, Change()), 3000);
이걸 이제 상태 기반으로 변경하고, 상태가 업데이트되면 updateDOM을 활용해 리렌더시키도록 변경시켜보자.
업데이트 -> 리렌더
현재의 업데이트코드를 리렌더 코드로 만들어주자.
render.js파일에 rerender 함수를 만들어줄 것이다.
rerender시 최상단 요소 App컴포넌트를 재호출해서 이전의 App과 비교하여 리렌더하는 형태다.
let prev; //이전의 virtual DOM
let root; //최상단 root요소
import App from '../components/App';
import { makeDOM } from './makeDOM';
import { updateDOM } from './updateDOM';
/**
* @param {Object} content
* @param {HTMLElement} container
*/
export const render = (content, container) => {
root = container; //최상단 요소를 최초렌더(mount)시 등록
prev = content; // 현재 virtual DOM을 최초 mount컴포넌트로 등록
const element = makeDOM(content);
container.appendChild(element);
};
export const rerender = () => {
const updateElement = App();
updateDOM(root, prev, updateElement);
prev = updateElement; //업데이트가 끝나면, 요소 업데이트
};
이제 index.js에서는 render만해주자.
index.js
import App from './components/App';
import React from './core/React';
const app = document.getElementById('root');
React.render(App(), app);
컴포넌트가 상태를 갖게하기
그럼 어떻게 해야 컴포넌트가 상태를 가질 수 있을까?
예시를 만들기 위해 App컴포넌트를 흔하디 흔한 counter로 바꿔주었다.
import React from '../core/React';
const Header = () => {
return <header>버튼 만들기</header>;
};
const App = () => {
return (
<div>
<Header />
<span>{1}</span>
<button>Click me!</button>
</div>
);
};
export default App;
저 span태그의 1을 상태로 관리해보자.
let 으로 변수를 선언해 관리
간단하게 let으로 변수를 선언하고, 변경시켜서 써보자
import React from '../core/React';
import { Header } from './Header';
let count = 0;
const App = () => {
return (
<div>
<Header />
<span>{count}</span>
<button
onclick={() => {
count++;
}}
>
Click me!
</button>
</div>
);
};
export default App;
자 이렇게 하면 클릭했을때 아무일도 일어나지 않을 것이다.
왜냐하면 rerender를 일으켜주지 않았기 때문이다.
그러므로 상태가 업데이트되면 rerender함수를 불러주자.
...
<button
onclick={() => {
count++;
rerender(); //다시그려
}}
>
Click me!
</button>
...
잘 작동된다!
문제점
자 그런데 이 방식에는 문제점들이 있다.
let 변수는 컴포넌트 내부에서 선언되지 않았다. 따라서 컴포넌트 내부에 let변수를 넣게되면 매번 0으로 초기화되게 될 것이다.
또, 매번 이벤트 콜백에 rerender를 호출해야한다는 문제가 있다.
이런 문제를 해결하기위해 클로저 형태로 변수를 관리하는 추상화된 함수인 useState를 구현해볼 것이다.
useState
useState는 초기값을 입력으로 받고, state와 setState함수를 담은 배열을 반환한다. 이걸 구현해보자.
import { rerender } from './render';
let state;
export const useState = (initialValue) => {
if (state === undefined) {
//현재 상태가 비어있으면
state = initialValue; //입력한 초기값으로 초기화해라
}
const setState = (updateValue) => {
state = updateValue; //value를 업데이트하고
rerender(); //리렌더해라
};
return [state, setState];
};
요로코롬 작성하면 App컴포넌트 내부에서 state를 관리할 수 있게된다.
import React from '../core/React';
import { useState } from '../core/useState';
import { Header } from './Header';
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<Header />
<span>{count}</span>
<button
onclick={() => {
setCount(count + 1);
}}
>
Click me!
</button>
</div>
);
};
export default App;
오 우리가 아는 그 코드가 됐다!
또다시 문제점
그냥 하나만 쓰는 경우는 문제가 없다.
그런데 useState를 2번 이상 쓰는 경우 문제가 발생하게 된다.
import React from '../core/React';
import { useState } from '../core/useState';
import { Header } from './Header';
const App = () => {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(10);
return (
<div>
<Header />
<div>Counter1</div>
<span>{count}</span>
<button
onclick={() => {
setCount(count + 1);
}}
>
Click me!
</button>
<div>Counter2</div>
<span>{count2}</span>
<button
onclick={() => {
setCount2(count + 1);
}}
>
Click me!
</button>
</div>
);
};
export default App;
분명히 두번째 Counter의 초기값은 10인데 0으로 초기화가 되고, 1번을 누르던 2번을 누르던 둘다 업데이트되는 꼴을 볼 수 있다.
왜일까?
바로 useState에 선언한 state가 값 하나이기 때문이다.
여러개의 state를 사용하려면 이걸 여러개를 담을 수 있도록 해주어야 한다.
배열을 활용해서 useState가 불릴때마다 새로운 state를 만들도록 처리해보자.
import { rerender } from './render';
const states = []; //배열 형태의 states
let stateIndex = 0;
export const useState = (initialValue) => {
const indexBind = stateIndex; //stateIndex가 ++형태로 변경되므로 bind시켜주는 변수
stateIndex++;
if (states[indexBind] === undefined) {
states.push(initialValue);
stateIndex++;
}
const setState = (updateValue) => {
states[indexBind] = updateValue;
rerender();
};
return [states[indexBind], setState];
};
오 그럼 업데이트가 잘될까??
아니, 안된다.
왜일까?
states배열을 useState가 return되기전에 console로 찍어서 확인해보자.
오잉? 왜이런걸까?
이걸 이해하기 위해서는 다시 렌더링 과정으로 돌아가야한다.
rerender가 호출되면 App컴포넌트를 다시 호출해서 최신 virtual DOM을 업데이트한다.
이때 App컴포넌트를 호출하는 과정에서 useState가 각각 한번씩 더 불리는 것이다.
따라서 index가 증가하게 되고, 새로운 state 2개가 생성되고 마는 것이다.
이를 방지하기 위해서는 리렌더시 useState의 index를 초기화하는 과정이 필요하다.
이를 함수로 만들어주자.
export const initializeIndex = () => {
stateIndex = 0;
};
rerender함수에서 불러주자.
export const rerender = () => {
initializeIndex(); //리렌더시 useState의 index를 초기화
const updateElement = App();
updateDOM(root, prev, updateElement);
prev = updateElement; //업데이트가 끝나면, 요소 업데이트
};
이제 업데이트가 잘 되는 것을 확인할 수 있다!
현재의 구현로직
사실 현재 구현한건 리액트처럼작동하게 만든거라 리액트와는 많이 다를 것이다만... 현재의 렌더링 로직을 정리해보자면
1. 화면 데이터를 받아온다.
2. 화면을 그린다.
3. 사용자가 상호작용을 한다.
4. state가 변한다.
5. rerender가 발생된다.
5. 화면 데이터를 다시 받아온다.
6. 화면을 그린다.
....
이런 로직이다.
이렇게 구현함으로써, flux패턴처럼 동작하게 되었다!
이제 기존의 리액트처럼 컴포넌트 작성하고, useState 훅을 통해 state를 관리할 수 있다!
https://github.com/d0422/make-react