Letโs build a React from scratch
- 1์ฅ VirtualDOM & Renderer
- 2์ฅ State Management & Hooks
- 3์ฅ React Suspence & Concurrent Mode
- 4์ฅ Server Side Rendering
- ๊ฐ์ : typescript ๊ฐ ์๋ tsc ์ฌ์ฉ ํด์ผํจ
-npx typescript --init
+npx tsc --init- ํ๋์ ์คํฌ๋ฆฝํธ๋ก ๋ฌถ๊ธฐ
"dev": "tsc -w & npx serve ."- React ๋ฅผ ๋ง๋ค์ด์ค
- React.createElement ๋ฅผ ์ถ๊ฐ ํด์ค
- HTML tag ๋ฅผ ์์ฑํ๋ฉด ๊ฐ์ฅ ํ์๋ถํฐ ์์๋ก ํ๊ทธ ๊ฐ์๋งํผ createElement ๋ฅผ ํธ์ถํจ
const App = (
<div draggable>
<h2>Hello React!</h2>
<p>I am a pargraph</p>
<input type="text" />
</div>
);['h2', null, 'Hello React!']
['p', null, 'I am a pargraph']
['input', {โฆ}]
['div', {โฆ}, undefined, undefined, undefined]- tree ์ ๋ณต์ ๋ณธ์ ์ ์ฅํ๊ธฐ ์ํด tag, props, children ์ ๋ถ๋ฆฌํด์ ๋ฐ์
- tag ๊ฐ fuction ์ธ ๊ฒฝ์ฐ ์ง์ ์คํํด์ el ๋ฐํ
<div id="myapp"></div>์ virtual DOM ์ root node ๋ก ์ก๊ณ ์ค์ DOM ์ ๋ ๋๋งํ๋ค
- ํด๋น type์ ์ค์ DOM node ์์ฑ
- props ๋ณต์
- children ์กด์ฌํ๋ ๊ฒฝ์ฐ 1~2 ๋ฅผ ๋ฐ๋ณตํ์ฌ ํ์ฌ DOM ํ์๋ก append
- container.appendChild(domEl) ์ผ๋ก ๋ธ๋ผ์ฐ์ container ์ ์ถ๊ฐ
- Text node ์ ๊ฒฝ์ฐ ๋ณ๋ ์ฒ๋ฆฌ ํ์
- ๋ฆฌ์กํธ ์ฒ์ ๋ฐฐ์ธ๋๋ ์ด๊ณ ์๋ค๋ง ์ง์ ๋ง๋ค๊ธฐ ํ๋์ค ์์์ ๊ฒ๋จน์๋๋ฐ ์๊ฐ ๋ณด๋ค ๊ฐ๋จํ๋ค.
- ์ฌ์ค JSX ๊ฐ ๋๋ฌด ๋ง์๊ฑธ ํด์ฃผ๋ ๊ฒ ๊ฐ๋ค. JSX ์ง์ ๋ง๋ค๊ธฐ๋ ๋์ ?
- virtual DOM ์ ํ๊ณ์ ๋ํ ๊ธ๋ค์ด ์์ฆ ๋ง์ด ๋ณด์ด๋๋ฐ, signal ๊ธฐ๋ฐ๋ ์ง์ ๋ง๋ค๊ธฐ ํด๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค
- ์ด๊ธฐ๊ฐ์ ๋ฐ์ state ์ setter ๋ฅผ return
const useState = (initialState) => {
console.log("useState is initialized with value:", initialState);
let state = initialState;
const setState = (newState) => {
console.log("setState is called with newState value:", newState);
state = newState;
};
return [state, setState];
};- ๋ชจ๋ ๊ฒ์ ๋ค์ ๋ค reRender ํ๊ธฐ
- onchange ๋ง๋ค reRender ๊ฐ ํธ์ถ๋๊ณ , useState ๊ฐ ํธ์ถ๋๋ฉด์ state ๊ฐ initialValue ๋ก reset ๋๋ค
- redering ์ค์ ์ ๊ฐ์ ์๋๋ค
- ํ์ฌ render ํจ์๋ append ๋ง ์ํ
// ---- Library --- //
const reRender = () => {
console.log("reRender-ing :)");
const rootNode = document.getElementById("myapp");
// reset/clean whatever is rendered already
rootNode.innerHTML = "";
// then render Fresh
render(<App />, rootNode);
};- state ๋ฅผ useState ๋ฐ์ ๋๊ณ ๋ณ๊ฒฝ๋์๋์ง ํ์ธํ์
// ---- Library --- //
let myAppState;
const useState = (initialState) => {
// Check before setting AppState to initialState (reRender)
myAppState = myAppState || initialState;
console.log("useState is initialized with value:", myAppState);
const setState = (newState) => {
console.log("setState is called with newState value:", newState);
myAppState = newState;
// Render the UI fresh given state has changed.
reRender();
};
return [myAppState, setState];
};-
library ์ ์ ์๋ก์ ์ผ๋ง๋ ๋ง์ state ๊ฐ ์ด๋์ ํ์ํ ์ง ๋ชจ๋ฅธ๋ค
-
์๋ก ๋ค๋ฅธ state ๊ฐ์ overwrite ๋์ด์๋ ์๋๋ค
-
cursor ๋ก ๊ด๋ฆฌํ๋ globalArray ์์ฑ
// ---- Library --- //
+const myAppState = [];
+let myAppStateCursor = 0;
const useState = (initialState) => {
// get the cursor for this useState
+ const stateCursor = myAppStateCursor;
// Check before setting AppState to initialState (reRender)
+ myAppState[stateCursor] = myAppState[stateCursor] || initialState;
console.log(
+ `useState is initialized at cursor ${stateCursor} with value:`,
myAppState,
);
const setState = (newState) => {
console.log(
+ `setState is called at cursor ${stateCursor} with newState value:`,
newState,
);
+ myAppState[stateCursor] = newState;
// Render the UI fresh given state has changed.
reRender();
};
+ // prepare the cursor for the next state.
+ myAppStateCursor++;
+ console.log(`stateDump`, myAppState);
+ return [myAppState[stateCursor], setState];
};- reRender ์์ myAppStateCursor ๋ฅผ ์ด๊ธฐํ
// ---- Library --- //
const reRender = () => {
// ..
rootNode.innerHTML = '';
+ // Reset the global state cursor
+ myAppStateCursor = 0;
// then render Fresh
render(<App />, rootNode);
};- state ๊ด๋ฆฌ๋ฅผ ์ํ global array ์กด์ฌ
- ์กฐ๊ฑด์ ์ด๋ ๋ฐ๋ณต๋ฌธ ์์์ ์กฐ๊ฑด์ ์ผ๋ก useState ์ ๊ฐ์ hook ์ด ํธ์ถ๋๋ค๋ฉด cursor ์ถ์ ํ๊ธฐ๊ฐ ์ด๋ ต๋ค
- state ๊ตฌํ์ด ์๊ฐ๋ณด๋ค ์ฌ์์ ๋๋๋ค.
- diffing ์ฒ๋ฆฌ๋ ๋ณ๋๋ก ์ํ๋๊ฑด์ง ์ฌ๊ธฐ์๋ง ๋จ์ํ ์ํจ ๊ฑด์ง ๊ถ๊ธํ๋ค.
- multi state ์ฒ๋ฆฌํ ๋ ๊ผญ cursor ๋ฅผ ๋ณ๋๋ก ๋ฌ์ผํ๋? map์๋ํ key ๋ก ์ฒ๋ฆฌํ ์๋ ์๋์ง? ๊ถ๊ธํ๋ค
- ์ input ์ onchange ๋ฅผ ์ ์ ํ๋๋ฐ๋ ์ฐ๋ฆฌ๊ฐ ํํ ํ๋ onChange ์ฒ๋ผ ๋์ํ์ง ์๊ณ input ๋ฐ์ผ๋ก ํฌ์ปค์ค๋ฅผ ์ฎ๊ฒจ์ผ๋ง ๋ฐ์๋๋๊ฒ์ผ๊น?
- React Suspense and Concurrent Mode
- traditional way: fetching after the initial render
- state ์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฑ์
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then((u) => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}- waterfall ๋จ์ : ์์กด๋ ๋ฐ์ดํฐ๊ฐ fetch ๋ ๋๋ง๋ค re-render ๋๋ค
- ์ปดํฌ๋ํธ์ ๋ํ ์ ๋ณด๋ฅผ ์ ์ฉ function call ๋ก ๋ถ๋ฆฌํ๋ค
- render ๋ฅผ trigger ํ๊ธฐ ์ํด์ setState ๋ ์ฌ์ ํ ์ฌ์ฉ
// Wrapping all data fetching
function fetchProfileData() {
return Promise.all([fetchUser(), fetchPosts()]).then(([user, posts]) => {
return { user, posts };
});
}
// Using it in our Component
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then((data) => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}- Start fetching
- Finish fetching
- Start rendering
- fetchProfileData ๋ฅผ ์ฌ์ฉ์ ์ํ๋๋ฐ?
- Start fetching
- Start rendering
- Finish fetching
- fetching ์ต์ ํ ๊ณ ๋ คํ ํ์๊ฐ ์๋ค
- fetching ์๋ฃ๋๋ฉด ํ๋ฒ๋ง ๋ ๋๋ง ํ๋ฉด ๋๋ค
- image, ๋ค๋ฅธํ์ด์ง, ๋ฌธ์ ๋ฑ์ non-blocking ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์๋ค
- : React render cycle ์์ async call ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๋ฉ์ปค๋์ฆ
- React ์ rendering ์ ์๋๋ synchrounous
- renderer ๋ VirtualDOM ์๋ง ์ ์ฉ
- DOM ์ ์ด๋ค ๋ถ๋ถ์ signal ์ ์ฃผ๊ณ ๊ธฐ๋ค๋ ค์ผํ๋์ง ๊ตฌ๋ถ ํ์
- ๋ชจ๋ promise ๋ค์ ์ถ์ ํด์ ์์ ์ด ๋๋๋ฉด ์๋์ผ๋ก rendering ์ํ
- ํญ์ ๋ถ๋ชจ-์์ ๊ตฌ๋ชจ๊ฐ ์๋ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด try-catch block ์ ์ปจ์ ์ ์ฐจ์ฉํ์ฌ ์์ง ๋ก๋ฉ์ค์ธ VirtualDOM tree ์ ๋ณด๋ฅผ ์ ์ก
Concurrent React ๋ ์ค๋จ ๊ฐ๋ฅํ rendering ์ด๋ค
- simulate slow image fetching
- ํ์ฌ๋ promise ์ฒ๋ฆฌ๋ฅผ ๋ชปํ๊ธฐ๋๋ฌธ์ ์๋ ์ฝ๋๊ฐ ์๋ฌ ๋๋๊ฒ ๋ง๋ค
// ---- Remote API ---- //
const photoURL = 'https://picsum.photos/200';
const getMyAwesomePic = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(photoURL), 1500);
});
};
//..
const App = () => {
//..
const photo = getMyAwesomePic();
return (
<h2>Our Photo Album</h2>
<img src={photo} alt="Photo" />
// ..- createResource() ๊ฐ async call ์ ์ถ์
// ---- Library ---- //
const resourceCache = {};
const createResource = (asyncTask, key) => {
// First check if the key is present in the cache.
// if so simply return the cached value.
if (resourceCache[key]) return resourceCache[key];
// If not then we need handle the promise here
// ....
};- resourceCache ์ async task ๋ณด๊ด
- key ์ ํด๋นํ๋ resource ๊ฐ ์กด์ฌํ๋ฉด ํด๋น task ๊ฐ ๋๋ฌ๋ค๋ ์๋ฏธ
- key ์์ผ๋ฉด? ์์ง resolved ๋์ง ์์์ผ๋ฏ๋ก ๋ ๋๋ง ํ๋ฉด ์๋จ
// ---- Library ---- //
const resourceCache = {};
const createResource = (asyncTask, key) => {
// First check if the key is present in the cache.
// if so simply return the cached value.
if (resourceCache[key]) return resourceCache[key];
// If not
+ throw { promise: asyncTask(), key };
};- key ์์ผ๋ฉด ๋ฐ๋ก throw ํด๋ฒ๋ฆฐ๋ค -> virtual DOM tree ์์ฑ ์ค๋จ
// ---- Application --- //
const App = () => {
//..
const photo = createResource(getMyAwesomePic, 'photo');
return (
//..- ํ์ฌ๋ Uncaught error ๋๋๊ฒ ์ ์
// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
if (typeof tag === "function") {
try {
return tag(props, ...children);
} catch ({ promise, key }) {
console.log(promise);
console.log(key);
}
}
//..
},
};- catch ๋ ๋์์ง๋ง ์ฌ์ ํ Promise ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ๋ชจ๋ฆ
- h2 ๋ก ๊ฐ๋จํ fallback UI ๋ฅผ ๋ง๋ค์ด๋ณด์
// ---- Library --- //
createElement: (tag, props, ...children) => {
//..
} catch ({ promise, key }) {
// We branch off the VirtualDOM here
// now this will be immediately be rendered.
return { tag: 'h2', props: null, children: ['loading your image'] };- promise ๊ฐ throw ๋ ๊ฒฝ์ฐ resourceCache ์ ๋ด์ ์ถ์
- ๋ค์ ๋ฒ loop ์์ resolved ๋์๋ค๋ฉด rerender ์์ ๋ฐ์๋จ
// ---- Library --- //
createElement: (tag, props, ...children) => {
//..
} catch ({ promise, key }) {
// Handle when this promise is resolved/rejected.
promise.then((value) => {
resourceCache[key] = value;
reRender();
});
//..- ๊ณผ์ : ํ์ฌ ๊ตฌํ์ผ๋ก๋ resource ๊ฐ 2 ๊ฐ ์ด์์ผ ๋ ๋ชจ๋ resource ๊ฐ resolved ๋์ด์ผ rendering ์ด ๋ ํ ๋ฐ ์ด๋ป๊ฒ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ํ ๊ฒ์ธ๊ฐ?
- Concurrent React ๋ ์ค๋จ ๊ฐ๋ฅํ rendering ์ด๋ค
- try/catch ๋ฅผ ์ด๋ฐ์์ผ๋ก ํ์ฉ ํ ์ค์ ๋ชฐ๋๋๋ฐ.. ์ด๊ฑด ์ ๋ฌด๋ก์ง์์๋ ํ์ฉ ํด๋ณผ ์ ์์ ๊ฒ ๊ฐ๋ค
- Suspense ๋ async call ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๋ฉ์ปค๋์ฆ์ด๋ค
- ๊ทธ๋ฅ ์ฑ๋ฅ์ ์ํ ๋ถ๊ฐ ๊ธฐ๋ฅ์ผ๋ก๋ง ์๊ฐํ๋ Suspense ์ ๋์ ๋ฐฉ์์ ์ดํด๋ณผ ์ ์์ด ์ข์๋ค
- ๋จ์ํ ๊ตฌ์กฐ์ resourceCache ๋ง์ผ๋ก Promise ๋ฅผ ์ถ์ ํ ์ ์๋๊ฒ ํฅ๋ฏธ๋ก์ ์ง๋ง,
- ์ค์ ๋ก๋ ๋ ๋ณต์กํ ๊ตฌ์กฐ๊ฐ ํ์ํ ๊ฒ ๊ฐ๋ค. eg) resource ๊ฐ ์คํจํ์ ๋ ์ฌ์๋ ํ๋ ๋ก์ง
- fetchProfileData ์์ ๋ ์คํ์ธ๊ฑฐ ๊ฒ ์ง?
- Cache ๊ด๋ จ ๊ณ ๋ฏผํด๋ณด์์ผ ํ ๊ฒ๋ค
- Cache invalidation์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์?
- Memory leak ๋ฐฉ์ง๋ฅผ ์ํ cleanup ์ ๋ต์?
- Concurrent Mode๊ฐ Fiber ์ํคํ ์ฒ์ ์ด๋ค ๊ด๋ จ์ด ์๋์?
- SWR ์ ๋ฐ์ดํฐ์ ๊ด์ ์์, Suspense ๋ ๋ ๋๋ง ๊ด์ ์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํ๊ฑฐ๊ฑฐ๋ผ๊ณ ๋ณด๋ฉด ๋ ๊น?