
📜 사실 노션에서 쓰고 옮긴거라 노션이 더 가독성 좋아요.
https://yopark.notion.site/React-ReactJS-23ab50b8bcfc42c788750844726f3d06
💡 노마드코더의 ReactJS로 영화 웹 서비스 만들기 강좌의 처음 ~ 3.4절 내용을 기반으로 작성하였습니다. 코드의 변화 과정을 극적으로 설명하기 위해, 강좌에서 등장하는 코드와는 일부 다르게 작성되었음을 알립니다.
“무턱대고 프레임워크부터 공부하지 말라” 라는 말이 있다.
저런 류의 걱정은 대부분 “일단 이전에 쓰던 기술에 익숙해지고 나야만, 이번 기술이 이전 기술의 어떤 문제점을 어떤 방식으로 개선했는지 깊게 이해할 수 있다” 라는 생각에서 비롯된다.
이 말에 100% 동감한다!
이번 글은 JavaScript로 토이프로젝트를 제대로 한번쯤 해본 사람에게 추천한다.
JavaScript로 토이프로젝트를 하나만 해봐도, document.querySelector로 요소를 고르고, innerText 프로퍼티로 HTML을 수정하는 일이 비일비재하게 일어난다는 사실을 체감한다. (프로젝트 사이즈가 작아서 불편함을 느끼지 못했을지 몰라도, 어딘가 최적화가 덜 된 느낌 정도는 충분히 받을 수 있다.)
이번 글을 읽고 React의 탄생 배경을 겉으로나마 이해하고 나면, React스럽게 코드를 짰을 때 JS 프론트엔드 코드가 얼마나 더 가독성 있어지는지, 얼마나 더 리소스가 절약되는지에 놀라움을 금치 못할 것이다.
💡 뭐… 말은 거창하게 React의 기술적 역사를 배우는 것처럼 써놓았지만, 단순히 Vanila JS 코드가 React스럽게 어떻게 변화하는지 설명하는 글입니다. 가볍게 봐주시기 바랍니다!
<!DOCTYPE html>
<html>
<body>
<h3>Total clicks: 0</h3>
<button id="btn">Click me</button>
<script>
<!-- 하단에 별첨 -->
</script>
</body>
</html>
버전 1.0의 HTML 코드
let counter = 0;
const h3 = document.querySelector("h3");
const button = document.getElementById("btn");
function handleClick() {
counter += 1;
h3.innerText = `Total clicks: ${counter}`;
}
button.addEventListener("click", handleClick);
버전 1.0의 JavaScript 코드

이미 짐작하셨다시피,
0에서 시작하여, 버튼을 누를 때마다 1씩 올라가는 코드다.
addEventListener()를 활용하여 버튼 클릭 시마다 handleClick()이 실행되도록 함.handleClick()이 실행될 때마다 counter 값을 증가시키며 ,innerText 프로퍼티를 활용하여 HTML을 직접 변경함.React.createElement()<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script>
<!-- 하단에 별첨 -->
</script>
</body>
</html>
버전 2.0의 HTML 코드. react와 react-dom 스크립트를 추가했다.
let counter = 0;
const root = document.getElementById("root");
const h3 = React.createElement("h3", null, "Total clicks: 0");
function handleClick() {
counter += 1;
const h3 = React.createElement("h3", null, `Total clicks: ${counter}`);
const container = React.createElement("div", null, h3, btn);
ReactDOM.render(container, root);
}
const btn = React.createElement("button", {
id: "btn",
onClick: handleClick
}, "Click me");
const container = React.createElement("div", null, h3, btn);
ReactDOM.render(container, root);
버전 2.0의 JavaScript 코드
div#root 밖에 없다.React.createElement()로 요소를 만들고ReactDOM.render()로 렌더링한다. 
React.createElement() 프로토타입React.createElement(
type,
[props],
[...children]
)
출처 : React 공식 문서 - createElement()
예시를 통해 각 인자로 어떤 것이 들어가는지 살펴보자.
// 예시 1
const h3 = React.createElement("h3", null, `Total clicks: ${counter}`);
// 예시 2
const btn = React.createElement("button", {
id: "btn",
onClick: handleClick
}, "Click me");
// 예시 3.1, 3.2
const container = React.createElement("div", null, h3, btn);
const container = React.createElement("div", null, [h3, btn]);
type : HTML 태그명이 들어간다. 예) “h3”, “button”, “div”[props] : 해당 요소의 id, style뿐 아니라, eventListener도 넣을 수 있다!eventListener("click", handleClick) → onClick: handleClick[...children] : 이미 만들어진 자식 요소를 넣을 수 있다. ... 은 Spread Parameter로 불리며, 가변 인자라는 뜻이다.💡 버전 2.0은 이론적으로만 존재하는 코드입니다. 실제 개발자들은 직접
React.createElement()를 호출하는 일이 없습니다! 그럼에도 버전 2.0의 코드를 왜 알아야 하는지 궁금하시다면, 여기를 눌러주세요. 버전 2.0의 코드는 특별합니다.
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
<!-- 하단에 별첨 -->
</script>
</body>
</html>
버전 3.0의 HTML 코드. babel 스크립트를 추가했다.
let counter = 0;
const root = document.getElementById("root");
function handleClick() {
counter += 1;
const H3 = () => (
<h3>Total Clicks: {counter}</h3>
);
const Container = () => (
<div>
<H3 /> <Button />
</div>
);
ReactDOM.render(<Container />, root);
}
const H3 = () => (
<h3>Total Clicks: 0</h3>
);
const Button = () => (
<button id="btn" onClick={handleClick}>
Click me
</button>
);
const Container = () => (
<div>
<H3 /> <Button />
</div>
);
ReactDOM.render(<Container />, root);
버전 3.0의 JSX 코드
JSX가 아직 안 와닿으시는 분들을 위해, 버전 2.0과 버전 3.0의 코드 일부를 가져왔다.
// 버전 2.0(JS)
const btn = React.createElement("button", {
id: "btn",
onClick: handleClick
}, "Click me");
// 버전 3.0(JSX)
const Button = () => (
<button id="btn" onClick={handleClick}>
Click me
</button>
);
비교해보니, 확실히 눈에 띈다.
더 HTML스럽지 않은가?
${} 을 사용하지 말고, 그냥 {} 만 쓰면 된다. // Before
h3.innerText = `Total clicks: ${counter}`;
// After
const H3 = () => (
<h3>Total Clicks: {counter}</h3>
);
{}를 잊지 않는다. <button id="btn" onClick={handleClick}>Click me</button>
H3 = () => (<h3></h3>); // 이 문법(Arrow func)이 어색하신 분들을 위해, 뒤에서 설명하겠습니다.
h3 = () => (<h3></h3>);
const Container = () => (
<div>
<H3 /> <h3 />
</div>
);
위 코드를 Babel에 넣어보자.
"use strict";
H3 = () => /*#__PURE__*/React.createElement("h3", null);
h3 = () => /*#__PURE__*/React.createElement("h3", null);
const Container = () => /*#__PURE__*/React.createElement(
"div",
null,
/*#__PURE__*/React.createElement(H3, null),
" ",
/*#__PURE__*/React.createElement("h3", null)
);
<H3 />는 정확히 사용자 정의 컴포넌트를 가리키지만, <h3 />으로 했을 때는 문자열로 처리하여 실제 HTML h3 태그를 만들도록 오작동한다.
따라서, 사용자가 h3 이라는 이름으로 컴포넌트를 만들었으면, 재사용이 불가능하다. (그리고 사람이 보기에도 <h3 /> 이 HTML h3를 의미하는지, 사용자 정의 h3를 의미하는지 알 방법이 없다)
// 예시 1. 잘못된 방법(오류)
// -> Container에서 재사용 안 하고 바로 render하는 경우라면, 정상 작동한다.
const H3 = (
<h3>Total Clicks: 0</h3>
);
// 예시 2. 올바른 방법
const H3 = () => (
<h3>Total Clicks: 0</h3>
);
const Container = () => (
<div>
<H3 />
</div>
);
React 컴포넌트는 페이지에 렌더링할 React 엘리먼트를 반환하는 작고 재사용 가능한 코드 조각입니다. 가장 간단한 React 컴포넌트는 React 엘리먼트를 반환하는 일반 JavaScript 함수입니다.
화면을 구성하는 데 자주 사용되는 UI(Button, Panel, Avatar), 혹은 복잡한 UI(App, FeedStory, Comment) 컴포넌트는 재사용 가능한 컴포넌트가 될 수 있습니다.
HTML의 요소(=엘리먼트)와는 달리, 개발자에 따라 상당히 주관적인 개념이다.
유일한 기준은 재사용 가능한, 하나의 기능을 수행하는 요소들의 묶음이라는 점이다.
function 키워드를 사용하지 않고, 함수를 더 간략화하여 정의하는 방법이다.
function f(a, b) {
return a + b
}
f = (a, b) => {
return a + b
};
// 내용이 return 한 줄밖에 없을 때, 중괄호 생략 가능
f = (a, b) => return a + b;
이 세 가지 표현은 동일하다.
추가적으로 몇 가지 얘기하자면,
() 는 써줘야 한다.f = () => return 1;f = a => return a*2;() 로 대체하여 사용한다.f = (a, b) => ( {foo: a, bar: b} );따라서, 아래 두 코드는 동일하다.
const Container = () => (
<div>
<H3 /> <Button />
</div>
);
function Container() {
return (
<div>
<H3 /> <Button />
</div>
);
}
JSX로 만든 HTML 태그 느낌의 코드는, 사실 객체로 변환되기 때문에, 바로 위에 소개된 3번 규칙에 따라 () 로 return을 대체한다.
개발자들이 사용하지도 않는, 사실 이론적으로만 존재한다고 할 수 있는 버전 2.0의 코드를 공부해야 했던 이유는, JSX로 작성된 버전 3.0의 코드를 Babel로 트랜스컴파일한 결과가 버전 2.0이기 때문이다.
JSX로 작성된 코드는
React.createElement()를 사용하는 형태로 변환됩니다.
JSX를 사용할 경우React.createElement()를 직접 호출하는 일은 거의 없습니다.
자세한 정보는 JSX 없이 React 사용하기 문서에서 확인할 수 있습니다. 인용 링크
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
<!-- 하단에 별첨 -->
</script>
</body>
</html>
버전 4.0의 HTML 코드. 버전 3.0에서 달라진 게 없다.
let counter = 0;
const root = document.getElementById("root");
function render() {
ReactDOM.render(<Container />, root);
}
function countUp() {
counter += 1;
render();
}
const Container = () => (
<div>
<h3>Total clicks: {counter}</h3>
<button onClick={countUp}>Click me</button>
</div>
);
render();
버전 4.0의 JSX 코드
비교를 위해 버전 3.0의 JSX 코드를 다시 가져왔다.
let counter = 0;
const root = document.getElementById("root");
function handleClick() {
counter += 1;
const H3 = () => (
<h3>Total Clicks: {counter}</h3>
);
const Container = () => (
<div>
<H3 /> <Button />
</div>
);
ReactDOM.render(<Container />, root);
}
const H3 = () => (
<h3>Total Clicks: 0</h3>
);
const Button = () => (
<button id="btn" onClick={handleClick}>
Click me
</button>
);
const Container = () => (
<div>
<H3 /> <Button />
</div>
);
ReactDOM.render(<Container />, root);
코드량이 극적으로 줄어든 것을 확인할 수 있다.
React.createElement() 에서 단일 요소 별로 만들고 합치던 관습에서 벗어나지 못했는데, 사실 JSX에서는 컴포넌트 하나에 자식 요소까지 한번에 정의가 가능하다. const Container = () => (
<div>
<h3>Total clicks: {counter}</h3>
<button onClick={countUp}>Click me</button>
</div>
);
만약 H3 , Button 이 다른 곳에서도 요긴히 쓰일 컴포넌트였다면 재사용을 위해 별도로 분리하여야 겠지만, 지금은 재사용을 안 할 것이라 짐작되기 때문에 합치는게 더 깔끔하다.
매번 새로운 Container() 를 새로 정의한 뒤에 렌더링할 필요가 없어졌다!
처음부터 {counter} 를 바운딩시켜놓으면, render() 만 해도 자동으로 바뀐 counter 값으로 새로 렌더링된다.
- 이전까지 새로 Container()를 정의할 수밖에 없었던 이유는, 초기 innerText 값을 단순히 Total Clicks: 0이라는 쌩 문자열로 할당해버렸기 때문이다.
ReactDOM.render(<Container />, root); 이 중복되기 때문에 함수 Wrapper를 만들었다.
Container()를 새로 정의할 필요가 없어졌다?이 사항은, 단순히 코드량을 줄이는 장점보다도 더 대단한 장점이 있다.
바뀐 부분(이번 코드에서는 숫자 부분)만 렌더링되기 때문에 실행하는데 드는 리소스를 아낄 수 있다는 점이다!
https://www.youtube.com/watch?v=-VPFbVnFzSU#t=645s 에서, 그 장점이 소개되어 있다.
[Before]

[After]

React.useState() function App() {
let [counter, setCounter] = React.useState(0);
const onClick = () => {
setCounter(counter+1);
};
return (
<>
<h3>Total clicks: {counter}</h3>
<button onClick={onClick}>Click me</button>
</>
);
}
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
counter와 counterUp() 는 없애고 React.useState()의 반환값을 이용하여 아주 깔끔하게 처리하였다. Container()는 컴포넌트의 느낌이 덜 살아서 App() 으로 RenameReact.useState() 을 이용하면 매번 렌더링해주지 않아도, 상태 변화 시 자동으로 렌더링한다. 따라서, render() Wrapper 함수는 필요 없어졌다. App()의 return 객체를 굳이 의미없는 <div></div>로 감쌀 필요 없다. 그 방법에 대하여 뒤에서 다룬다.
useState는 인자로 초기 state 값을 하나 받습니다.
useState는 현재의 state 값과 이 값을 업데이트하는 함수를 쌍으로 제공합니다.
let [counter, setCounter] = React.useState(0); // 초기값: 0
setCounter() 를 이용하여 상태를 변경할 수 있으며,
counter 를 이용하여 상태의 현재 값을 받아올 수 있다.
하지만 저렇게 반환된 counter 값을 counter += 1 과 같은 방식으로 직접 변경하는 것은 효력이 없다. 여기서의 counter 는 이전까지 쓰던 변수 역할과는 달리, getCounter 느낌이라고 생각하는 것이 훨씬 이해하기 편할 것이다.
div 대신 Fragment를 사용하자[Before]

[After]

굳이 <div></div> 로 감쌀 필요가 없을 때는, After 처럼 div를 없앨 수 있다.
코드는 다음과 같다.
// 예시 1. 기본형(React.Fragment)
return (
<React.Fragment>
<h3>Total clicks: {counter}</h3>
<button onClick={onClick}>Click me</button>
</React.Fragment>
);
// 예시 2. 축약형(빈 채로 둠)
return (
<>
<h3>Total clicks: {counter}</h3>
<button onClick={onClick}>Click me</button>
</>
);
setCounter() 를 Safe하게! function App() {
let [counter, setCounter] = React.useState(0);
const onClick = () => {
/* setCounter(counter+1); */
setCounter(current => current+1);
};
return (
<>
<h3>Total clicks: {counter}</h3>
<button onClick={onClick}>Click me</button>
</>
);
}
current => current+1 은 함수형 프로그래밍 방식으로 상태를 변화시킨다.
왜… 굳이 이렇게 바꿔야 할까?
노마드코더님께 한번 질문해보았다.

첫번째 추측이 틀렸다.
동시에 setCounter() 에 접근하는 경우는 발생하지 않을 것이라는 니꼬님의 답변이다.
대국민 찬반 투표 App이니 하는 예시는 한번 읽고 다 잊어주시기 바란다.
setCounter()가 동기적이지 않기 때문?함수형 프로그래밍 방식을 사용하는 이유에 대해 검색한 결과, React 공식 문서에서 관련된 내용을 발견할 수 있었다.
const [state, setState] = useState(initialState);
최초로 렌더링을 하는 동안, 반환된 state(state)는 첫 번째 전달된 인자(initialState)의 값과 같습니다.
다음 리렌더링 시에 useState를 통해 반환받은 첫 번째 값은 항상 갱신된 최신 state가 됩니다.
즉, 아무리 setState() 로 여러번 값을 바꾼다 하더라도, ReactDOM.render() 를 실행하지 않는 한 getState() (= state) 는 같은 값을 계속 반환한다는 뜻이다.
const onClick = () => { // counter=100
setCounter(counter+1);
console.log(counter); // 100
setCounter(counter+1);
console.log(counter); // 100
}; // render() 이후에는 counter=101
이 상황을 두고 전문용어로, React.useState()가 비동기적이라고 한다.
setState()를 써도 재깍재깍 state가 바뀌지 않으니까(요청을 다 처리하지 않고 뒤로 미루니까) 동기적이지 않다고 볼 수 있겠다.
setCounter()가 재깍재깍 적용되도록 하기 const onClick = () => { // counter=100
setCounter(counter => counter+1); // re-render
console.log(counter); // 101
setCounter(counter => counter+1); // re-render
console.log(counter); // 102
}; // render() 이후에도 counter=102 유지.
함수적 갱신을 계속 하면, 리렌더링을 너무 자주하여 리소스가 낭비되겠지만,
디버깅이 훨씬 어려운 오작동을 만들어낼 가능성이 있으니 니꼬 님이 권장했으리라 짐작했다.
그리고 ReactDOM.render() 는 바뀐 부분에 대해서만 리렌더링을 진행하기 때문에 이전 상태에서 상태가 변하지 않았다면 리렌더링을 아예 스킵한다! 그러니 리소스 걱정을 크게 할 필요는 없는 것이다.
이제 독자분들은, 진짜로 React의 기술적 탄생 배경을 읽을 준비가 되었습니다.
드디어 “새로운 기술인 React와 JSX가 이전 기술인 JavaScript의 어떤 문제점을 어떤 방식으로 개선했는지” 깊게 이해해볼 기회가 주어졌습니다.
더 전문적인 내용이 궁금하다면, 아래 글도 읽어보세요!