리액트 공식 문서 읽기 (v17.0.2)
공식문서가 친절하지 않아요.
어떤 공식문서든 전제가 깔려있어요 → 그 전제가 무엇일까?
난이도가 들쭉날쭉한 공식문서 → 쉽다고 훅 하고 넘어가도 될까?
느껴봅시다.
또한 공식문서는 읽고 적용하고 다시 읽어봐야해요. (한 번에 이해가 될리가 없잖아요!)
모든 건 도구 👉 페인모인츠를 해결하기 위한 것.
하지만 보통 how to에 집중을 하죠.
이 도구가 어떤 문제를 해결하기 위해서 (why) 등장했는지 알아야 해요.
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
javascript
가 아니네!ReactDOM.render
는 virtual DOM을 만들어주는 친구입니다.Javascript를 확장한 문법인 건 알겠는데…
공식문서가 무슨 말을 하는지 잘 모르겠어요.
딱 봐도 이해를 위한 공식문서라기 보다는 자랑하는 거 같아..
훅훅 읽어 갑시다.
중괄호 안에 javascript가 들어갈 수 있군요.
👉 여기서 왜?
값, 식만 넣을 수 있구나.
문은 안되는구나!
(공식문서를 읽으면서 값식문을 떠올릴 수 있다면 당신이 바로 고수)
JSX는 결국 JS의 React.createElement로 변환되기 때문에 객체의 value값으로 당연히 문을 쓸 수가 없죠! 값과 식만 가능합니다.
JSX는 HTML보다는 Javascript에 가깝기 때문에 camelCasae 프로퍼티 명명 규칙을 사용합니다.
👉 여기서 왜? 라는 의문이 들어야합니다.
React.createElement
바벨 입장에서 리액트도 사실 많은 플러그인들 중에서 하나일 뿐이라서 이렇게 바꿔줄 수도 있어요.
위의 이야기와 같죠. JSX -> JS의
React.createElement
객체로 변환됩니다.
특정 상황에서는 createElement
를 직접 작성해야하는 경우도 생겨요.
React 앱의 가장 작은 단위
React Element는 일반 객체입니다. (DOM element와는 다름)
⚠️ 컴포넌트와 엘리먼트는 달라요. 엘리먼트가 컴포넌트의 구성요소죠. 더 작아요.
❗ 처음에는 헷갈린다면 넘어가요. 너무 하나씩 이해를 하기 위해 시간을 소비한다면 좋지 않습니다.
일반적으로 하나의 루트 DOM 노드가 있어요.
Virtual DOM 는 여러 개가 있을 수 있죠. 하지만, 각 Virutal DOM은 데이터를 공유할 수 없어요.
어, Real DOM HTML 앱에서 로그인 부분은 React 하고 싶어! 👉 render로 삽입
어, 이 부분도 React로 하고 싶어!👉 render로 삽입
이렇게 점진적으로 Virtual DOM을 적용할 수는 있지만 여러 개의 Virtual DOM은 데이터를 공유할 수 없어요. (여러 개의 앱 인거죠.)
리액트 입장에서는 Virtual DOM 하나가 하나의 앱입니다.
React 엘리먼트는 불변객체입니다. (런타임 상황에서!)
✋ 잠깐~
코드는 항상 두 가지 상태가 있어요.
1. 코드 에디터 상의 컴파일 상태
런타임 상태
불변이라니 결국 UI를 업데이트하는 유일한 방법
은 새로운 엘리먼트를 생성하고 이를 ReactDOM.render 한테 전달하는 것 뿐이겠네요.
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
tick이 호출될때마다 늘 새로운 element를 전달하게 됩니다. (런타임에서 새롭게 생성)
🤔 흠... 그래도 바뀐 부분만 업데이트되면 좋지 않을까? 낭비같아요. 아래에서 이야기 해보죠.
어? 위의 Tick을 해보니 inspector로 관찰하면 전부 새로 만드니 다 바뀌어야할 것 같은데 실제로는 시간부분만 번쩍번쩍 바뀌네요?
😀 Virtual DOM 등장
Javascript DOM selector와 Real DOM 사이에
Virtual DOM이 등장했을까!
👉 이 컨셉은 컴퓨터 세계에서 정말 자주 등장하는 컨셉이예요. 보통 속도 차이가 너무 크거나 다루는 cost가 차이가 너무 큰 경우죠. 예를 들면,
사용자에게 살짝 기다리게 하고 유튜브 동영상과 플레이어 사이에 저장소를 하나 끼워두고 일정 수준 이상 받아지면 그 때 보여주게 하자! (버퍼링 개념)
HDD의 정보를 CPU까지 와야하는데 HDD 읽는 건 너무 느려요. 그래서 그 사이에 RAM을 만들었어요.
✋ Virtual DOM도 마찬가지죠.
Real DOM을 Javascript로 핸들링하기 안 좋아요.
Real DOM 입장에서는 이전 상황과 같은지 다른지를 판단할 수가 없어요.
그렇다면 다루기 쉬운 친구(virtual DOM)를 끼워넣고 이전 상황과 비교를 할 수 있다면 좋겠다!
개념적으로 컴포넌트는 JavaScript 함수와 유사합니다. “props”라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환합니다.
// 함수 컴포넌트
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 클래스 컴포넌트
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
🤔 함수와 클래스의 가장 큰 차이점?
모두 React에게 있습니다.
🤔 클래스 컴포넌트에서는 이렇게 하면 어떨까?
class Welcome extends React.Component {
render(props) {
return <h1>Hello, {props.name}</h1>;
}
}
Welcome.render()
를 사용할 수 있을 것 같은데 말이죠.위처럼 작성하여 사용자가 임의로 render를 호출하여 DOM을 조작하면 문제가 생길 수 있을 거예요.
😋 안정적인 렌더링을 위해 VirtualDOM
를 온전히 React가 담당해야겠죠.
마찬가지로 함수 컴포넌트도 코드 상으로 직접 호출하는 경우는 없죠. (호출해도 아무 일도 일어나지 않아요.)
function Welcome(props) {
return <h1>Hello, {props.name}</h1>; // createElement 1번째 호출
}
const element = <Welcome name="Sara" />; // createElement 2번째 호출
ReactDOM.render(
element,
document.getElementById('root')
);
DOM tree처럼 createElement가 nesting 되어 호출되어요.
즉, 최상위 아래에 붙어있는 경우가 되겠네요.
❗ 잠깐~ 컴포넌트 이름은 항상 대문자로 시작합시다.
소문자로하면 바벨은 이게 사용자 정의 컴포넌트인지, html 컴포넌트인지 알 길이 없어요. 그래서 welcome(사용자 정의 컴포넌트)을 그냥 h1, div 태그처럼 일반 문자열로 인식을 해버려요.
createElement의 첫번째 인자가 Welcome 함수 입니다.
createElement는 우리가 호출했어요.
하지만, Welcome 함수는 createElement 안 쪽에서 리액트가 호출한 게 되겠죠.
상태는 변화하는 값입니다. 프론트 앱은 결국 상태를 UI로 변경하는 함수라고 볼 수 있어요. 중요합니다.
// Clock 컴포넌트는 날짜 생성 로직이 없어짐으로써 재사용성이 늘어났어요.
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
👉 setInterval없이 안될까?
함수 -> 클래스로 변경해야합니다.
생성 -> 변화 -> 죽음
state를 메모리에 저장해야하는데 함수는 호출마다 생애주기가 새로 시작되죠.
생애주기를 가질 수 있는 class
로 변환합시다. class는 instance를 만들고 제거하기 전까지 변화
가 허용되죠.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
🤔 근데 왜 setState로 값을 변경하죠?
초기에는 변수가 값이 할당될 때를 런타임에서 감지할 수가 없었어요. 메서드 호출을 통해서 감지를 한거죠.
지금은 js의 프록시 라는 기능이 있긴한데 여전히 지금 그대로 쓰고 있어요.
함수 호출은 React가 합니다. (어떤 트리거인지는 다음에 배울게요.)
class는 무조건 render 메서드가 존재하고 render를 실행하면 함수 컴포넌트처럼 createElement로 객체가 만들어지게 됩니다.
✨ 하지만 다른 점이 있죠. class는 인스턴스가 존재한다면 이전의 인스턴스를 사용하여 render를 호출하겠죠. 그렇다면 기존의 상태를 이용할 수 있어요.
👉 지금은 함수 컴포넌트에서 상태를 가지고 있지만 이 당시에는 함수에 상태를 줄 수 있는 방법을 고안하지 못했어요. 그래서 class 컴포넌트가 주를 이루었던 거죠!
React.Component를 확장하는 동일한 이름의 ES6 class를 생성합니다.
render()라고 불리는 빈 메서드를 추가합니다.
함수의 내용을 render() 메서드 안으로 옮깁니다.
render() 내용 안에 있는 props를 this.props로 변경합니다.
남아있는 빈 함수 선언을 삭제합니다.
함수는 당연히 인자로 props를 받겠죠.
클래스는 당연히 생성자가 받아요.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
키워드는 리액트와 컴포넌트입니다. 서로의 상황을 서로 알고 싶다!
react <-> component
이 사이의 관계를 봐야해요. 아래 이야기는 이벤트 핸들러
와 굉장히 유사해요.
😢 class 컴포넌트는 자기 자신의 render 메서드를 호출할 수 없죠. 그러니 virtual DOM이 갱신되지 않고 UI도 갱신되지 않겠죠.
결국 호출은 당연히 react가 해줘야합니다.
🤔 그런데 react는 그 업데이트 시점을 어떻게
알 수 있을까요?
setState 메서드
를 주게 되었습니다.마찬가지로 컴포넌트 입장에서도 Virtual DOM에 언제 붙는지 알아야겠죠?
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
// ✨ 이 부분이죠
componentDidMount() {
// DOM에 rendering된 후에 실행됩니다.
this.timerID = setInterval(
() => this.tick(),
1000
);
}
// ✨ 이 부분이죠
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
🤔 어 근데 Timer는 왜 state로 관리하지 않죠?
this.timerID = setInterval(
() => this.tick(),
1000
);
애초에 상태가 아니죠? 변하는 값이 아닙니다. 유지해야하는 값이 아닙니다.
1. 직접 수정 금지
2. 업데이트는 비동기적일 수도 있어요.
😀 동기적으로 하려면 setState 인자에 함수를 넘겨줍시다.
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
🤔 근데 왜 함수는 동기적으로 동작할까요?
3. State 업데이트는 병합됩니다.
부모 컴포넌트(A)는 state를 props로 자식 컴포넌트(B)에게 보낼 수 있어요.
⚠️ 하지만 자식(B)은 부모(A)가 누군지, 어떻게 동작하는지 몰라야 재사용이 가능해요.
👉 이런 부분에서 캡슐화 라고 불러요. 실제 js로 컴포넌트가 캡슐화되어 있지는 않지만 구조적으로 캡슐화가 되어 있어요.
😀 skip~ 까다롭지 않네요.