ReactJS 번역 및 정리한 내용. 언어 기능이 생겨 현재는 일부 한글로 번역되어 있는 상태이다. 사내 발표용으로 작성하였다.
- Component와 Props
- State와 생명주기
- 이벤트 처리하기
Component와 Props
- Component는 UI를 재사용 가능한 독립적인 조각으로 분리시켜줍니다.
- 개념상 Component는 JS의 function과 같습니다.
함수형 컴퍼넌트와 클래스 컴퍼넌트
Componenet를 정의하는 가장 간단한 방법
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// ES6 lambda
const welcome = props => <h1>Hello, {props.name}</h1>;
이 함수는 데이터를 가진 하나의 props
(props는 속성을 나타내는 데이터) 객체 인자를 받은 후 React 엘리먼트를 반환하므로 유효한 React 컴퍼넌트입니다. 이러한 컴퍼넌트는 JS 함수이기 때문에 말 그대로 함수 컴퍼넌트
라고 호칭합니다.
또한 ES6 class
를 사용하여 컴퍼넌트를 정의할 수 있습니다.
class Welcome extends React.Component {
render() {
return <h1>Hello, {props.name}</h1>;
}
}
컴퍼넌트 렌더링
이전까지는 React 엘리먼트를 DOM 태그로 나타냈습니다.
const element = <div />;
React 엘리먼트는 사용자 정의 컴포넌트로도 나타낼 수 있습니다.
const element = <Welcome name="Sara" />;
React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트를 해당 컴포넌트에 단일 객체로 전달한다. 이 객체를 props
라고 합니다.
다음은 페이지에 “Hello, Sara”를 렌더링하는 예시입니다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
이 예시에서는 다음과 같은 일들이 일어납니다.
<Welcome name="Sara" />
엘리먼트로ReactDOM.render()
를 호출한다.- React는
{name: 'Sara'}
를 props로 하여Welcome
컴포넌트를 호출한다. Welcome
컴포넌트는 결과적으로<h1>Hello, Sara</h1>
엘리먼트를 반환한다.- React DOM은
<h1>Hello, Sara</h1>
엘리먼트와 일치하도록 DOM을 효율적으로 업데이트한다.
컴퍼넌트 합성
컴퍼넌트는 자신이 렌더링 될 때 다른 컴퍼넌트를 참조할 수 있다. React 앱애서는 버튼, 폼, 다이얼로그, 화면 등의 모든것들이 컴퍼넌트로 표현된다.
const Welcome = (props) => (<h1>Hello, {props.name}</h1>);
const App = (props) => (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
컴퍼넌트 추출
- Component를 더 작은 Component로 나누는 것을 두려워 하지맙시다.
- 아래 예에서는 어떻게 나누는게 좋을까요?
const Comment = (props) => (
<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 className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
- props로
author
(객체),text
(문자열),date
(Date)를 받고, Comment, 소셜 미디어 사이트 정보까지 받고 있다. - 이 Component는 중첩된 구조로 수정하기 어렵고, 각 부분을 재활용 하기도 어렵다.
먼저 Avatar
를 분리해보죠.
const Avatar = (props) => (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
- Avatar는
Comment
가 어떻게 렌더링 되는지 알 필요가 없음. author
보다 일반적인user
를 할당한 이유.- 필요한 만큼만 알면 된다.
이제 Comment
가 일부 단순해졌습니다.
const Comment = (props) => (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
다음으로 Avatar
옆에 사용자의 이름을 렌더링 하는 UserInfo
를 추출합시다.
const UserInfo = (props) => (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
Comment가 더욱 단순해졌습니다.
const Comment = (props) => (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
- 처음에는 지루한 작업처럼 보일 수 있음.
- 그러나 나중에 프로그램이 커지게 되면 두각을 나타냄.
- 자주 쓰이는 아주 작은 단위 부터(
Button
,Panel
,Avatar
) - 그 자체로도 이미 복잡한(
App
,FeedStory
,Comment
) - 것들은 재사용 가능한 컴퍼넌트로 만드는 것이 좋음.
Props는 읽기 전용이다.
다음 sum
함수를 봅시다.
const sum = (a, b) => a + b;
이 함수는 순수함수이다. 입력값을 바꾸지 않고, 동일한 입력에 동일한 결과를 반환하기 때문이다.
반면 아래 함수는 자신의 입력값을 변경하기 때문에 순수 함수가 아니다.
const withdraw = (account, amount) => {
account.total -= amount;
}
모든 React 컴퍼넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.
React 컴포넌트는 state를 통해 위 규칙을 위반하지 않고 사용자 액션, 네트워크 응답 및 다른 요소에 대한 응답으로 시간에 따라 자신의 출력값을 변경할 수 있습니다.
State를 보러 갑시다.
State와 생명주기
전에 본 시계 예시 기억나시나요? 다시 한번 살펴봅시다.
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);
rendering을 다시 콜 해서 Component를 갱신 시키는 방법으로 해결 했었죠? 이번에는 Clock
Component가 재사용, 캡슐화 가능하게 만들어 볼게요. 아마 스스로 업데이트 할거에요.
캡슐화부터 해볼게요.
const Clock = (props) => (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
const tick = () => {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
그러나 중요한게 빠졌죠? Clock
이 타이머를 설정하고 매초 UI를 업데이트 하는 것이 Clock
의 구현 세부사항이 되어야 합니다.
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
- 이걸 구현하려면 우리는
state
라는 녀석을Clock
Component에 추가해 주어야 해요. - 앞에서도 설명 했지만
state
를 사용하려면 Class를 써야 합니다. - 업데이트로 인해 함수형 컴퍼넌트에서도 state를 다룰 수 있게 되었습니다.
- 그렇지만, 일단 클래스로 진행하도록 하겠습니다.
클래스로 바꾸기
React.Component
를 확장하는 동일한 이름의 class를 생성합니다.render()
라고 불리는 빈 메서드를 추가합니다.- 함수의 내용을
render()
메서드 안으로 옮깁니다. render()
내용 안에 있는props
를this.props
로 변경합니다.- 남아있는 빈 함수 선언을 삭제합니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock
은 이제 함수가 아닌 클래스로 정의됩니다.
render
메서드는 업데이트가 발생할 때마다 호출되지만, 같은 DOM 노드로 <Clock />
을 렌더링하는 경우 Clock
클래스의 단일 인스턴스만 사용됩니다. 이것은 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 해줍니다.
클래스에 로컬 State 추가하기
render()
함수에서this.props.date
를this.state.date
로 바꿀게요.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- class에
생성자
를 추가해주고this.state
를 초기화 시킬게요.
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>
);
}
}
어떻게 props를 생성자에 전달하죠?
// 이렇게 props를 인자로 받고
constructor(props) {
// 이렇게 super에 props를 전달해주어야 합니다.
super(props);
this.state = {date: new Date()};
}
클래스 컴퍼넌트는 항상 props로 기본 constructor를 호출해야 합니다.
- date props를
에서 삭제할게요.
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
완성된 코드:
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>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
잠시만요. 타이머 코드는 어디에 갔죠? 이제 Clock
이 혼자 업데이트 하도록 해볼게요.
생명주기 메서드를 클래스에 추가하기
많은 Component가 있는 응용프로그램에서는 Component가 릴리즈 될 때 사용된 리소스를 확보 하는 것이 중요합니다.
- Clock이 처음 DOM에 렌더링 될 때 타이머를 설정해야 합니다.( mount )
- Clock에 의해 생성된 DOM이 삭제될 때 타이머를 해제해야 합니다.( unmount )
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
/// Component가 mount 되면 실행됨.
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
/// Component가 unmount 되면 실행됨.
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>
);
}
}
componentDidMount에서
타이머를 설정했고, componentDidUnmount에서
타이머를 해제했습니다.
근데 타이머 아이디를 어떻게 저장한거죠?
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
this.props
는 React에 의해 설정되고, this.state
는 특수한 의미가 있습니다. 그런데, 타이머ID 같이 데이터 흐름 안에 포함되지 않는 어떤 항목을 보관할 필요가 있으면 클레스에 부가적인 필드를 추가해도 됩니다.
state를 업데이트 하기위해 tick()
함수 내에서 this.setState()
를 사용합니다.
요약:
<Clock />
이ReactDOM.render()
에 전달되면 React는Clock
의 생성자를 호출한다. 그럼 현재 시간이 포함된 객체로 state를 초기화 한다.- React는
Clock
컴퍼넌트의render()
함수를 호출. 이 render 함수가 React가 DOM에 무엇을 표시해야 하는지 결정하는 방법이다. 그 다음 React는 DOM을 업데이트 해서Clock
렌더링 출력과 일치시킨다. Clock
의 출력이 DOM에 삽입되면, React는componentDidMount()
생명주기 메서드를 호출한다. 그 안에서 Clock 컴퍼넌트는 매초 컴퍼넌트의tick()
메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청함.- 매 초 마다 브라우저가
tick()
메서드를 호출함. 그 안에서는setState()
로 현재 시간을 계속 넘겨줘서 UI 업데이트를 진행한다.setState()
를 호출하는 덕분에 React는 상태가 변경되었다는 것을 감지하고,render()
함수를 다시 호출한다.this.state.date
가 달라졌기 때문에render()
의 결과값이 달라지고, 렌더링에 업데이트된 시간이 포함된다. React는 이에 따라 DOM을 업데이트 한다. componentWillUnmount()
덕분에 페이지 이동등의 액션으로 component가 DOM에서 제거되면 타이머도 같이 제거된다.
State를 올바르게 사용하기
직접 State를 수정하지 마세요
// Wrong
this.state.comment = 'Hello';
대신에 setState()를 사용합니다.
// Correct
this.setState({comment: 'Hello'});
this.state
를 지정할 수 있는 유일한 공간은 바로 생성자입니다.
State 업데이트가 비동기적일 수 있어요
React는 여러 setState()
호출을 하나의 업데이트로 일괄처리하여 성능을 높입니다.
따라서 this.props
와 this.state
가 즉시 업데이트 되는 것이 아니라 비동기 적으로 업데이트 될 수 있으므로 다음 상태를 계산하기 위해 이 값에 의존하면 안됩니다.
예를 들어, 다음 코드는 카운터 업데이트에 실패할 수 있습니다.
// wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
이를 수정하기 위해 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용합니다. 그 함수의 첫 번째 인자는 이전 state이고, 두 번째 인자는 업데이트가 적용된 시점의 props 입니다.
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
State 업데이트는 병합됩니다
아래 state를 봐주세요. posts, comments가 있습니다.
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
이제 이걸 2번에 걸쳐서 업데이트를 해볼게요.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
2번 째 this.setState({ comments })
가 호출될 때 this.state.posts
의 데이터는 온전히 남아있고, this.state.comments
의 데이터는 완전히 대체 됩니다.
데이터는 아래로 흐릅니다
부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트가 유상태인지 또는 무상태인지 알 수 없고, 그들이 함수나 클래스로 정의되었는지에 대해서 관심을 가질 필요가 없습니다.
그래서 State는 로컬변수라고 불리거나, 캡슐화 되었다고 불립니다. 그 이유는 다른 Component에서 접근이 안되기 때문입니다.
그리고 부모 Component의 state를 자식 Component의 prop으로 전달할 수 있습니다.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
이것 또한 사용자 정의 컴퍼넌트에도 적용 가능합니다.
<FormattedDate date={this.state.date} />
const FormattedDate = (props) =>
(<h2>It is {props.date.toLocaleTimeString()}.</h2>);
이걸 일반적으로 “top-down(하향식)” 혹은 “단방향” 데이터 흐름이라고 부릅니다. 모든 state는 특정 Component가 소유하고, 해당 State에서 파생된 모든 데이터는 아래에만 영향을 미칩니다.
트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 수원(water source)이라고 할 수 있습니다.
이벤트 처리하기
HTML과 React 이벤트 처리 방식의 차이:
- React 이벤트는 소문자보다 camelCase를 사용합니다.
- JSX에서는 문자열보다 이벤트핸들러 함수를 전달해서 사용합니다.
HTML:
<button onclick="activateLasers()">
Activate Lasers
</button>
React:
<button onClick={activateLasers}>
Activate Lasers
</button>
또 다른 차이점으로는 이벤트에 false
를 리턴해서 이벤트를 정지할 수 없습니다. 반드시 명시적으로 preventDefault
를 호출해야 합니다.
HTML:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
React:
const ActionLink = () => {
const handleClick = (e) => {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
여기서 e
는 합성 이벤트입니다. React는 W3C 명세에 따라 합성 이벤트를 정의하기 때문에 브라우저 호환성에 대해 걱정할 필요가 없습니다.
React를 쓸 때 일반적으로 addEventListener
를 사용할 필요가 없습니다. 대신 Element가 처음 렌더링 될 때 리스너를 제공해주세요.
ES6 클래스를 사용할 때 일반적인 패턴으로 event handler가 클래스의 메서드가 되도록 하는 것입니다:
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>
);
}
}
JSX 콜백에서 this
의 의미에 대해 주의해야 합니다. JS에서는 클래스 메서드가 기본적으로 바인딩 되지 않습니다. 생성자에서 바인딩 해주지 않으면 onClick
메서드가 undefined
일거에요.
이건 React의 행동이 아닙니다. JS에서 함수가 작동하는 방식의 일부입니다. 일반적으로 onClick={this.handleClick}
과 같이 ()
가 없는 함수콜을 하려면 메서드를 를 바인딩 해야합니다.
이렇게 바인딩 하는것이 귀찮으세요? 아래처럼 실험적인 신택스를 사용해보세요.
class LoggingButton extends React.Component {
// 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
// 주의: 이 문법은 *실험적인* 문법입니다.
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
CRA(create-react-app)
을 사용하시면 기본적으로 활성화 됩니다.
만약 클래스 필드 문법을 사용하고 있지 않다면, 이렇게 화살표 함수를 사용할 수 도 있어요.
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
그런데 이렇게 하게되면 이 Component가 생성될 때 마다 다른 콜백이 생성됩니다. 대부분의 경우 문제가 되지 않으나, 콜백이 하위 컴퍼넌트에 prop으로서 전달된다면 그 컴퍼넌트들은 추가로 다시 렌더링을 수행할 수도 있습니다. 이러한 종류의 성능 문제를 피하고자, 생성자 안에서 바인딩하거나 클래스 필드 문법을 사용하는 것을 권장합니다.