- 리액트 공식문서 학습
- JSX
- Component
- state
- 이벤트 처리
- Form
- 합성, 상속
- Life Cycle
@@ 오늘은 리액트에 대해서 학습하는 시간을 가졌다. 공식문서를 토대로 함수형 컴포넌트와 클래스 컴포넌트를 배우고, 컴포넌트 내에서 변화하는 값인 state와 부모 컴포넌트가 자식 컴포넌트에게 전달해주는 Props에 대한 개념을 학습했다. 컴포넌트의 생명 주기인, 라이프 싸이클에 대해서 알아보았고, input, textarea등 Form 태그를 리액트 컴포넌트로 작성하는 법을 학습했다. 그리고 배운 개념을 토대로 이 전에 html, css로 구성했던 트위틀러 스프린트를 리액트로 다시 구현하는 시간을 가졌다.
생각보다 레슨은 쭉쭉 나가게 되어서, Lifting State Up 과 단방향 데이터 흐름 등 내일 배울 내용도 미리 예습하는 시간을 가졌는데 자식 컴포넌트가 부모컴포넌트의 상태에 접근해 어떻게 간접적으로 상태를 조작할 수 있는지도 학습해보는 시간을 가졌다.
리액트가 아닌 순수 자바스크립트 코드를 통해 배우도록 레슨 문서가 짜있었는데, 오히려 그 구조를 파악하는게 좀 어렵게 느껴졌다. (그렇기때문에 리액트를 쓰는 것이겠지만). 여튼 요지는 상태를 조작하는 함수를 자식 함수에 넘겨주고, 자식함수는 필요할 때 그 함수를 실행시킬 수 있다는 것이 핵심 요점이었다.
JSX 속성 정의
JSX는 주입 공격( XSS ) 을 방지한다.
바벨은 JSX를 React.createElement() 호출로 컴파일한다.
엘리먼트 렌더링
리액트로 구현된 애플리케이션은 일반적으로 하나의 루트 돔 노드가 있다
리액트를 기존 앱에 통합하려는 경우 원하는 만큼 많은 수의 독립된 루트 돔 노드가 있을 수 있다.
리액트 엘리먼트를 루트 돔 노드에 렌더링하려면 ReactDOM.render()로 전달하면 된다.
리액트 엘리먼트는 불변 객체다.
해당 엘리먼트를 생성한 이후에는 자식이나 속성을 변경할 수 없다.
UI를 업데이트하는 유일한 방법은 새로운 엘리먼트를 생성하고 이를 ReactDOM.render() 로 전달하는 것이다.
import React from "react";
import ReactDOM from "react-dom";
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);
위 코드는 매초 전체의 ui를 다시 그리도록 엘리먼트를 만들었지만 ReactDOM은 내용이 변경된 텍스트 노드만 업데이트 한다.
JSX 콜백 안에서 this의 의미에 대해 주의해야 한다. JavaScript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않다.this.handleClick을 바인딩하지 않고 onClick에 전달하였다면, 함수가 실제 호출될 때 this는 undefined가 된다.
일반적으로 onClick={this.handleClick}과 같이 뒤에 ()를 사용하지 않고 메서드를 참조할 경우 해당 메서드를 바인딩해야한다.
bind 호출하지 않고 해결하는 방법
1) 퍼블릭 클래스 필드 문법.
class LoggingButton extends React.Component {
// 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
// 주의: 이 문법은 *실험적인* 문법입니다.
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
2) 화살표 함수 사용
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
자바스크립트 함수와 유사
Props라는 임의의 입력을 받은 후 화면에 어떻게 표시하는지를 기술하는 React 엘리먼트를 반환
함수 컴포넌트 와 클래스 컴포넌트
함수컴포넌트
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
클래스 컴포넌트
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
각각 다른 추가 기능들이 존재한다.
컴포넌트 렌더링
리액트가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달한다. 이 객체를 props 라고 한다.
import React from "react";
import ReactDOM from "react-dom";
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(element, document.getElementById("root"));
컴포넌트 합성
컴포넌트 추출
props는 읽기 전용이다.
함수 컴포넌트나 클래스 컴포넌트 모두 컴포넌트의 자체 props를 수정해서는 안된다.
순수함수
입력값을 바꾸려고 하지않고 항상 동일한 입력값에 대해 동일한 결과를 반환
순수함수가 아닌 예
function withdraw(account, amount) {
account.total -= amount;
}
모든 리액트 컴포넌트는 자신의 props를 다룰 때 순수함수처럼 동작해야 한다.
리액트 컴포넌트는 위 규칙을 위반하지 않고, 사용자 액션, 네트워크 응답 및 요소에 대한 응답으로 시간에 따라 자신의 출력값을 변경할 수 있다.
조건부 렌더링
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
엘리먼트 변수
엘리먼트를 저장하기 위해 변수를 사용할 수 있다.
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
);
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
);
}
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
더 짧은 구문을 사용하고 싶을 때
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
JavaScript의 논리 연산자 &&를 사용하면 쉽게 엘리먼트를 조건부로 넣을 수 있다.
JavaScript에서 true && expression은 항상 expression으로 평가되고 false && expression은 항상 false로 평가. 따라서 && 뒤의 엘리먼트는 조건이 true일때 출력. 조건이 false라면 React는 무시하게 된다.
조건부 연산자 활용
condition ? true: false
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
컴포넌트가 렌더링하는 것을 막기
다른 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶을 때, 렌더링 결과를 출력하는 대신 null을 반환
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(state => ({
showWarning: !state.showWarning
}));
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
리스트와 Key
여러개의 컴포넌트 렌더링
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
컴포넌트 안에서 리스트 렌더링
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
위 코드를 실행하면 각 항목에 key를 넣어야한다는 경고가 표시
key
엘리먼트 리스트를 만들때 포함해야하는 특수 문자열 어트리뷰트
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')
);
키로 컴포넌트 추출
키는 주변 배열의 컨텍스트 에서만 의미가 있다.
function ListItem(props) {
// 맞습니다! 여기에는 key를 지정할 필요가 없습니다.
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 맞습니다! 배열 안에 key를 지정해야 합니다.
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
ListItem 컴포넌트를 추출 한 경우 ListItem 안에 있는 <li>
엘리먼트가 아니라 배열의 <ListItem />
엘리먼트가 key를 가져야 한다.
map()함수 내부에 있는 엘리먼트에 key 를 넣어주는 게 좋다.
key는 형제사이에서만 고유한 값이면 된다.
리액트에서 key는 힌트를 제공하지만 컴포넌트로 전달하진 않는다.
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
위 예제에서 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없다.
JSX 안에도 중괄호를 사용하면 map()함수의 결과를 처리할 수 있다.
직접 state를 수정하면 안된다.
직접 수정하게 되면 컴포넌트를 다시 렌더링하지 않는다.
대신 setState()를 사용한다.
this.state 를 지정할 수 잇는 유일한 공간은 constructor이다.
state 업데이트는 비동기적일 수 도 있다.
리액트는 성능을 위해 여러 setState()호출을 단일 업데이트로 한꺼번에 처리할 수 있다.
this.props와 this.state가 비동기적으로 업데이트 될 수 있기 때문에 state를 계산할 때 해당값에 의존해선 안된다.
이를 수정하기 위해 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용
이전 state와 업데이트 적용된 시점의 props 를 인자로 받아 사용한다.
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
));
state 업데이트는 병합된다.
setState()를 호출할 때 리액트는 제공한 객체를 현재 state로 병합한다.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
병합은 얕게 이루어지기 때문에 this.setState({comments})는 this.state.posts에 영향을 주진 않지만 this.state.comments는 완전히 대체된다.
데이터는 아래로 흐른다.
state 캡슐화, 로컬
특정 컴포넌트가 유상태, 무상태인지 알수없고, 함수나 클래스로 정의되었는지 관심을 가질 필요가 없다.
하향식, 단방향식 데이터 흐름
모든 스테이트는 항상 특정한 컴포넌트가 소유. 그 state 로부터 파생된 ui 또는 데이터는 오직 트리구조에서 자신의 아래에있는 컴포넌트에만 영향을 미친다.
모든 컴포넌트는 완전히 독립적이다.
참고 코드
리액트에서 이벤트는 캐멀케이스를 사용
JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달
리액트에선 false를 반환해도 기본 동작을 방지할 수 없다.
반드시 preventDefault를 명시적으로 호출해야 한다.
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
리액트는 W3C에 따라 합성 이벤트를 정의한다. 브라우저 호환성에 대해 걱정할 필요가 없다.
리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다.
클래스 컴포넌트에서 이벤트 정의는 보통 클래스의 메서드로 만든다.
on,off 상태를 토글하는 버튼을 렌더링하는 토글 컴포넌트
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
HTML에서 <input>, <textarea>, <select>
와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다.
React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 한다.
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'Please write an essay about your favorite DOM element.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('An essay was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Essay:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
React에서 <textarea>
는 value 어트리뷰트를 대신 사용합니다. 이렇게하면 <textarea>
를 사용하는 폼은 한 줄 입력을 사용하는 폼과 비슷하게 작성
React에서는 selected 어트리뷰트를 사용하는 대신 최상단 select태그에 value 어트리뷰트를 사용
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite flavor:
<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>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
<select multiple={true} value={['B', 'C']}>
<input type="file" />
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
Is going:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
this.setState({
[name]: value
});
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
UI가 아닌 기능을 여러 컴포넌트에서 재사용하기를 원한다면, 별도의 JavaScript 모듈로 분리하는 것이 좋다.
컴포넌트에서 해당 함수, 객체, 클래스 등을 import 하여 사용할 수 있다.
state and Lifecycle
클래스 컴포넌트로의 변경
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
클래스에 로컬 State 추가하기
생명주기
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>
);
}
}
생명주기 메서드
this.props가 React에 의해 스스로 설정되고 this.state가 특수한 의미가 있지만, 타이머 ID와 같이 데이터 흐름 안에 포함되지 않는 어떤 항목을 보관할 필요가 있다면 자유롭게 클래스에 수동으로 부가적인 필드를 추가해도 된다. (this.timerID)