ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
ReactDOM.render(element, container[, callback])
결국 리액트가 하는 일은 React element를 container DOM에 렌더링하는 것
해당 React엘리먼트가 이전에 container 내부에 렌더링된 적이 있다면, 이전 React element와 비교하여 변경이 필요한 DOM만을 변경한다.
callback이 인자로 전달되면 컴포넌트가 렌더링되거나 업데이트된 후 실행된다.
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
바벨은 위와 같은 JSX표현식을 다음과 같이 React.createElement()호출로 컴파일한다.
const element = React.createElement(
'h1', // type
{className: 'greeting'}, // [props]
'Hello, world!' // [...children]
);
React.createElement의 호출로 다음과 유사한 자바스크립트 객체, 즉 React element가 생성된다.
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
이와 같이 JSX는 정규 자바스크립트 함수(React.createElement)의 호출을 통해 자바스크립트 객체(React element)로 평가되는 자바스크립트 표현식이다
또한, React element는 생성된 이후 자식이나 속성을 변경할 수 없는 불변객체이다.
결국 리액트가 하는 것은 이 자바스크립트 객체를 읽어서 가상 DOM을 만들고 DOM diffing 알고리즘을 통해 최소한의 DOM조작만을 하는 것이다.
const title = response.potentiallyMaliciousInput;
// 이것은 안전합니다.
const element = <h1>{title}</h1>;
위와 같이 사용자의 입력값을 JSX안에 사용하는 것은 안전하다
React DOM은 기본적으로 렌더링하기 이전에 JSX에 삽입된 모든 값을 escape한다.(escape...?)
아마도 React.createElement에 넘겨줄 인자값으로 변환하는 과정에서 일종의 validation이 일어난다는 것 같은데 무슨 소린지 모르겠다
그렇기 때문에(?) 애플리케이션에는 명시적으로 작성된 내용 외에는 포함될 수가 없다
모든 항목(?)은 렌더링되기 전에 문자열로 변환된다.
즉, XSS(cross-site-scripting)공격으로부터 안전하다.
JSX가 컴파일된 결과인 React element는 리액트 애플리케이션을 구성하는 최소 단위이다.
(컴포넌트는 React element보다 상위의 요소이다)
이 React element는 불변 객체이기 때문에 UI를 업데이트하고자 한다면 새로운 React element를 생성하여 이를 ReactDOM.render로 전달하는 수밖에 없다.
컴포넌트는 props를 입력값으로 받아 React element를 반환하는, 일종의 자바스크립트 함수이다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
const element = <Welcome name="Sara" />;
리액트는 위와 같은 사용자 정의 컴포넌트를 발견하면 속성(name)과 자식(여긴 없음)을 해당 컴포넌트에 단일 객체(props)로 전달하는 것
// props
{ name: "Sara" };
인자값(props) 자체를 수정하지 않고, 항상 동일한 입력에 대해 동일한 결과값을 반환해야 한다는 의미이다.
한 이벤트로부터 여러 개의 state가 갱신될 수 있으므로, state하나가 바뀔 때마다 동기적으로 DOM을 업데이트하면 성능상 좋지 못하다.
따라서 한 이벤트로부터 파생된 state의 변동들을 가상 DOM에 한꺼번에 반영해 실제 DOM의 조작 횟수를 줄이기 위해서 state의 업데이트는 비동기적으로 처리한다.
this.setState((state, props) => ({
// 인자로 들어오는 state은 prevState값
// 인자로 들어오는 props는 갱신이 완료된 props
counter: state.counter + props.increment
}));
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
<button onclick="activateLasers()">
Activate Lasers
</button>
<button onClick={activateLasers}>
Activate Lasers
</button>
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
인자로 들어오는 e는 합성 이벤트(synthetic event)이다.
render() {
const count = 0;
return (
<div>
{ count && <h1>Messages: {count}</h1>}
</div>
// <div>0</div>
);
}
위와 같이 false로 평가될 수 있는 표현식을 반환할 때는 주의해야 한다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
리액트에서 엘리먼트 리스트를 렌더링하기 위해서는 위와 같이 key라는 문자열 속성을 설정해주어야 한다.
이렇게 엘리먼트 리스트에 key를 설정해주어야 하는 이유는 각 엘리먼트에 고유성을 부여하여 변경, 추가, 삭제할 엘리먼트를 효율적으로 식별하기 위함이다.
즉, 해당 데이터를 고유하게 식별할 수 있는 문자열을 key로 사용해야 하기에 보통은 해당 데이터의 ID를 사용한다.
리스트의 인덱스를 key로도 사용할 수 있지만, 만약 데이터의 순서가 바뀔 수 있는 경우 성능 저하나 state와 관련된 문제가 발생할 수 있기에 가능한 지양해야 한다.
(명시적으로 key를 지정해주지 않을 경우 리액트는 index를key로 사용한다)
HTML의 Form 엘리먼트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다.
반면 React는 Form엘리먼트의 state까지도 컴포넌트의 state로써 관리한다.
이렇게 React에 의해 값이 제어되는 Form엘리먼트를 controlled component라고 한다.
<textarea>
Hello there, this is some text in a text area
</textarea>
<textarea value={this.state.value} onChange={this.handleChange} />
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
<select multiple={true} value={['B', 'C']}>
HTML, JSX
값이 읽기 전용이기 때문에 uncontrolled-component이다.
File API로 조작할 수 있다.
<input type="file" />
범용적인 '박스' 역할을 하는 컴포넌트는 어떤 엘리먼트를 자식으로 가질 지 사전에 특정할 수 없는 경우도 있다. 이러한 컴퍼넌트에서는 자식 엘리먼트를 children prop으로 전달하는 것이 좋다.
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
// FancyBorder의 자식 엘리먼트들은 명시적으로 props로 넘겨주지 않아도 아래와 같이 참조할 수 있다.
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
한 컴포넌트가 어떤 컴포넌트의 보다 특수한 경우인 경우 역시 컴포넌트 합성으로 구현할 수 있다.
(공식 문서의 뉘앙스로 봐서는 상속과는 명백히 구분되는 개념인 모양이다)
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
컴포넌트도 함수와 마찬가지로 오직 한 가지 기능만을 가지도록 구현하는 것이 바람직하다.
사용자 상호작용 없이 렌더링만 가능한 버전으로 구현
이 단계에서는 컴포넌트 간에 데이터를 전달하기 위해 오직 props만 사용한다.
state는 오직 상호작용을 위해서만 사용해야 한다.
앞서 만든 컴포넌트 계층 구조에서 bottom-up, 즉 가장 하위의 컴포넌트부터 구현하는 것이 권장된다.
부모로부터 props로 전달되지 않을 것
사용자와의 상호 작용으로 변해야 할 것
컴포넌트 내부의 다른 state나 props를 통해 계산할 수 없을 것
3단계에서 찾아낸 state의 최소 집합이 각각 어떤 컴포넌트에서 관리되어야 할 지 결정
state를 기반으로 렌더링하는 모든 컴포넌트 확인
해당되는 컴포넌트들을 자식 컴포넌트로 가지는 부모 컴포넌트 확인
사용자와의 상호 작용이 state를 관리하는 컴포넌트보다 하위 컴포넌트에서 일어난다면, 해당 컴포넌트까지 setState를 props로 내려줘서 해당 컴포넌트가 부모 컴포넌트의 state를 변경할 수 있도록 구현
리액트는 container에 React element를 렌더링한다.
JSX가 컴파일되면 React element가 된다.
이 React element를 기능 단위로 조합한 것이 컴포넌트이다.
리액트는 데이터의 흐름을 명시적으로 보이게끔 하기 위해 단방향 데이터 흐름을 갖는다.
단방향 데이터 흐름에서 부모 컴포넌트는 자식 컴포넌트로 props를 통해 데이터를 내려준다.
사용자와의 상호 작용으로 UI가 변해야 할 경우 관련된 데이터를 state로 관리한다.