이 글은 리액트 공식 문서의 주요 개념 부분을 정독하며 작성하였음
공식 문서의 주소는 여기를 클릭
JSX는 JS에 XML을 추가해 확장한 문법이다. 리액트의 가장 작은 단위이자 화면에 표시할 내용을 기술하는 엘리먼트(element)를 생성하며 JS의 모든 기능이 포함되어 있다. 리액트를 이용할 때 필수로 사용해야 하는것은 아니지만 JS 코드 안에서 UI 작업을 할 때 시각적으로 도움되고 에러 및 경고 메세지를 표시해 사용자에게 편의성을 제공한다.
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
중괄호 안에 모든 유효한 JS 표현식을 넣어 사용할 수 있다.
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
컴파일이 끝나면 JSX가 JS 객체로 인식되기 때문에 코드와 같이 if구문 안에서 인자로서 받아들이고 반환이 가능하다.
const element = <a href="https://www.reactjs.org"> link </a>;
const element = <img src={user.avatarUrl}></img>;
속성에 따옴표를 사용해 문자열을 정의하거나 속성에 중괄호를 사용하여 JS 표현식 삽입이 가능하다.
하지만 따옴표나 중괄호 둘 중 하나만 사용해야하며, 동일한 속성에 두 가지를 동시에 사용하면 안 된다.
const element = <img src={user.avatarUrl} />;
태그가 비어있으면 />를 이용해 닫아주어야 한다.
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
JSX는 React.createElement()로 컴파일 되기 때문에 위의 두 코드는 동일하며 아래와 같은 객체를 생성한다.
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
이런 객체를 리액트 엘리먼트라고 하며 화면에서 표현하고자 하는 표현이다. 리액트는 이 객체를 읽어 DOM을 구성하고 최신 상태로 유지하는데 사용한다.
엘리먼트는 화면에 표시할 내용을 기술하는 리액트의 가장 작은 단위이다. 컴포넌트와는 다른 개념이며 리액트 DOM은 리액트 엘리먼트와 일치하도록 DOM을 업데이트 한다. 리액트 엘리먼트는 불변객체이기 때문에 자식이나 속성을 변경할 수 없고 UI를 업데이트 하는 유일한 방법은 새로운 엘리먼트를 생성해 ReactDOM.render()
로 전달하는 것이다.
<div id="root"></div>
이 div안에 들어가는 모든 엘리먼트를 리액트 DOM에서 관리하기 때문에 루트 DOM 노드라고 부른다.
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
리액트 엘리먼트를 루트 DOM 노드에 렌더링하려면 ReactDOM.render()로 전달하면 된다.
setInterval()을 이용해 초마다 전체 UI를 다시 그리게 만들었지만
React DOM은 내용이 변경된 노드만 업데이트 시킨다.
리액트에서 컴포넌트는 JS의 함수와 개념적으로 유사하며 props(이하 속성)
라고 하는 임의의 입력을 받은 후 화면에 표시되는 방식을 담고있는 엘리먼트를 반환한다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
위 함수 컴포넌트는 데이터를 가지고 있는 하나의 속성(props) 객체를 받아
리액트 엘리먼트를 반환하기 때문에 리액트 컴포넌트이며, JS함수이기에 함수 컴포넌트라고 호칭한다.
const element = <div />;
이렇게 DOM 태그를 이용해 리액트 엘리먼트를 나타낼 수도 있지만
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
위와 같이 사용자 정의 컴포넌트로도 나타낼 수 있다.
리액트가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하게 되면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달하게 되는데 이 객체를
props
라고 한다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
컴포넌트는 출력에 있어서 다른 컴포넌트를 참조할 수 있다.
위의 코드는 Welcome 컴포넌트를 여러번 렌더링 하는 App 컴포넌트 이다.
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
</div>
);
}
컴포넌트를 추출 함으로서 여러 개의 작은 컴포넌트로 소분해 재사용이 용이하게 만들 수 있다.
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
// 먼저 Avatar을 컴포넌트로 추출한 뒤 Comment내에 렌더링 된다는 것을 알 필요가 없기에
// props의 이름을 author에서 일반화된 user로 변경한다
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
//UserInfo 또한 컴포넌트로 추출해 props의 이름을 user로 변경한다.
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
//중첩 구조로 이루어져 있던 코드를 한줄로 나타낼 수 있다.
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
컴포넌트 추츨은 UI가 여러 번 사용되거나, 자체적으로 복잡하게 이루어진 경우 별도의 컴포넌트로 나누어서 만드는게 좋다.
function withdraw(account, amount) {
account.total -= amount;
}
모든 리액트 컴포넌트는 자신의 props를 다룰 때 반드시 입력값을 바꾸려 하지 않고 항상
동일한 입력값에 대해 동일한 결과를 반환하는 순수 함수처럼 동작해야 한다.
위 코드는 자신의 입력값을 변경하기 때문에 순수 함수가 아니다.
State
는 컴포넌트 내부에서 변경될 수 있는 값이다. 생명주기에 따라 변경될 수 있는 정보를 보유한 객체이며 부모 컴포넌트가 설정하고 값을 변경할 수 없는 props
와는 다르게 컴포넌트 자체적으로 내부에서 값을 업데이트 할 수 있다는 차이점이 있다.
class Clock extends React.Component {
constructor(props) {
1️⃣super(props);
this.state = {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')
);
위와 같은 클래스 컴포넌트에서 State를 사용할 때 초기 State를 지정하는 constructor을 추가하는데
항상 props로 기본 constructor을 호출(1️⃣)한 뒤 사용해야 한다.
컴포넌트 클래스에서 특별한 메소드를 선언하여 컴포넌트가 마운트(mount)되거나 언마운트(unmount)될 때 일부 코드가 작동될 수 있다. 이러한 메소드들을 생명주기 메소드라고 부른다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
//출력물이 DOM에 렌더링 된 후 실행되는 메소드
componentWillUnmount() {
clearInterval(this.timerID);
}
//타이머 분해 메소드
tick() {
this.setState({
date: new Date()
});
}
//컴포넌트 State를 업데이트하기 위한 setState()를 사용하는 메소드
// Wrong
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});
setState()를 사용하지 않고 수정하면 컴포넌트를 다시 렌더링하지 않는다.
리액트 엘리먼트에서 이벤트 처리 방식은 몇 가지 문법 차이는 존재하지만 JS DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사하다.
//in HTML
<button onclick="activateLasers()">
Activate Lasers
</button>
//in React
<button onClick={activateLasers}>
Activate Lasers
</button>
//in html
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
//in React
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
리액트에서는 false를 리턴해도 기본 동작을 방지할 수 없기에 preventDefault()를 호출해야 한다
리액트에서 DOM 엘리먼트가 생성된 후 리스너를 추가하기 위해 addEventListener을 호출할 필요 없이 엘리먼트가 처음 렌더링될 때 리스너를 제공하면 된다.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 콜백에서 `this`가 작동하려면 아래와 같이 생성자 안에서 바인딩 해주어야 한다
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
일반적으로 이벤트 핸들러를 클래스의 메소드로 만들어 사용한다.
만약 this.handleClick을 생성자 안에서 바인딩하지 않고 호출하게 되면 this는 undefined가 된다.
리액트에서는 원하는 동작을 캡슐화하는 컴포넌트를 만들 수 있다. 어플리케이션의 상태에 따라 컴포넌트 중 몇개만을 렌더링 할 수 있는데 JS에서 조건 처리와 같이 동작한다.
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')
);
isLoggedIn의 값에 따라 다른 컴포넌트를 출력시킨다.
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')
);
LoginControl 이름의 상태 컴포넌트에서 현재 상태에 맞게 로그인 버튼 or 로그아웃 버튼을
출력의 수정 없이 조건부로 렌더링 한다.
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')
);
&& 뒤의 표현식은 조건이 true일때 출력 되지만 조건이 false라면 무시하고 건너뛴다.
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')
);
위의 코드 처럼 렌더링될 때 컴포넌트 자체를 숨기고 싶을 때 null을 반환하면(⭐)
생명주기 메소드에 영향을 주지 않고 컴포넌트를 숨길 수 있다.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
리액트에선 위의 코드와 같이 map 함수를 이용해 엘리먼트 모음을 만들고 중괄호를 이용해 JSX에 포함이 가능하다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
//<li>{number}</li> (x)
<li key={number.toString()}>
{number}
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
컴포넌트 내에서 리스트를 렌더링 하는것이 일반적이다.
주석 처리된 코드를 삽입하면 키를 넣어야 한다는 경고 문구가 출력된다.
키는 리액트가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트이다. 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
키를 선택할 때 가장 좋은 방법은 다른 항목들 사이에서 유니크한 값이어야 한다.
대부분 데이터의 id를 키로 사용한다.
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')
);
키는 map 함수 내부에 있는 엘리먼트에 넣어주는 것이 좋다.
그리고 키는 배열 안에서 형제 사이에서 고유해야 하고 전체 범위에서 고유할 필요는 없다.
결론적으로 두 개의 다른 배열을 만들 때 동일한 키를 사용할 수 있다.