First written: 22-12-17
Uploaded: 22-12-28
Last modified: 23-03-01
Component는 기능별로 템플릿을 분리할뿐 아니라, 데이터를 주고받으며 UI를 변경해주고, Lifecycle API를 이용해 특정 lifecycle에 실행될 함수를 구현하기도 하며, 임의 메서드를 만들어 특정 기능을 구현할 수 있는 공간이라고 생각하면 된다. React의 화면은 이런 Component들의 집합이라고 생각하면 된다.
(src/App.js)
import { Component } from 'react';
import Sample from './Sample';
function getRandomColor() {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
class App extends Component {
state = {
color: '#000000'
}
handleClick = () => {
this.setState({
color: getRandomColor()
})
}
render() {
return (
<div>
<button onClick={this.handleClick}>Random Color Generator</button>
<Sample color={this.state.color} idNumber={1}>SAMPLE</Sample>
</div>
);
}
}
export default App;
(src/Sample.js)
import { Component } from 'react';
import PropTypes from 'prop-types';
class Sample extends Component {
static defaultProps = {
ungivenProp: 'Default Value'
}
static propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
state = {
number: 0,
message: '',
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'},
],
nextId: 6,
nameInput: ''
}
handleClick = () => {
this.setState((prevState, props) => {
return {number: prevState.number+1}
}, () => console.log('first'));
this.setState(prevState => ({
number: prevState.number+1
}), () => console.log('second'));
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
})
}
handleKeyPress = (e) => {
if(e.key === 'Enter'){
this.setState({
[e.target.name]: ''
})
}
}
handleClickRef = () => {
this.input.focus();
}
handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
this.setState({
names: [
...this.state.names,
{ id: this.state.nextId, text: this.state.nameInput }
],
nextId: this.state.nextId + 1,
[e.target.name]: '',
})
}
}
handleDoubleClickFilter = (id) => {
const nextNames = this.state.names.filter(name => name.id !== id);
this.setState({
names: nextNames
});
}
render() {
const name = 'Jinwoo';
const { color, idNumber, children, ungivenProp } = this.props;
const style = {
color: color
};
const namesList = this.state.names.map(name => <li key={name.id} onDoubleClick={() => this.handleDoubleClickFilter(name.id)}>{name.text}</li>)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>Props part</h2>
<p>You are {name}, right?</p>
<p style={style}>And our parent gives us {color} props that defines text color of this sentence.</p>
<p>Also children value: {children}</p>
<p>If some prop is required, and when it's not given, there will be error in console. : {idNumber}</p>
<p>If there is un given prop, you can give default value({ungivenProp}) to that prop.</p>
</div>
<div>
<h2>State and Event part</h2>
<p>Changing Number: {this.state.number}</p>
<button
className="number"
onClick={this.handleClick}
>increase +2</button>
<input type="text" name="message"
value={this.state.message}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
</div>
<div>
<h2>Ref part</h2>
<input type="text"
ref={ref => this.input=ref}
/>
<button
onClick={this.handleClickRef}
>
Focus to Input
</button>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={this.state.nameInput}
onChange={this.handleChange}
onKeyPress={this.handleKeyPressMap}
/>
</div>
</>
);
}
}
export default Sample;
이번 글에서는 위 두 코드를 기반으로 props, state, event, ref에 대해 살펴볼 것이며 자주 쓰이는 map, filter 문법도 같이 살펴볼 것이다.
Props는 부모에서 자식에게 (알려)주는 데이터라고 생각하면 된다. State는 해당 component가 가지고 있는 정보로, 주로 바뀔 가능성이 있는 정보들을 담아둔다. Event는 Javascript로 event를 줘본 적이 있다면 무리없이 사용할 수 있을 것이다. Ref는 javascript의 document.querySelector
와 비슷한 역할을 하는데, 부모가 자식 component 내부의 메서드를 활용하거나 혹은 state로 해결할 수 없는 경우에 DOM을 직접 조작하기 위해 사용한다. 여기 예시에서는 focus 메서드를 활용해보았다.
지나치게 긴 위의 코드를 필요한 부분만 잘라서 보자.
(src/App.js)
import { Component } from 'react';
import Sample from './Sample';
function getRandomColor() {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
class App extends Component {
state = {
color: '#000000'
}
handleClick = () => {
this.setState({
color: getRandomColor()
})
}
render() {
return (
<div>
<button onClick={this.handleClick}>Random Color Generator</button>
<Sample color={this.state.color} idNumber={1}>SAMPLE</Sample>
</div>
);
}
}
export default App;
handleClick
메서드나 state
의 정확한 쓰임새는 곧 알아보자. Sample component의 상위 component인 App에서 color, idNumber 그리고 children props를 Sample component에 전달하고 있다. Children prop라고 함은 <Sample>
태그 사이에 쓰여있는 text, 여기서는 SAMPLE
을 뜻한다.
(src/Sample.js)
import { Component } from 'react';
import PropTypes from 'prop-types';
class Sample extends Component {
static defaultProps = {
ungivenProp: 'Default Value'
}
static propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
render() {
const { color, idNumber, children, ungivenProp } = this.props;
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>Props part</h2>
<p style={style}>And our parent gives us {color} props that defines text color of this sentence.</p>
<p>Also children value: {children}</p>
<p>If some prop is required, and when it's not given, there will be error in console. : {idNumber}</p>
<p>If there is un given prop, you can give default value({ungivenProp}) to that prop.</p>
</div>
</>
);
}
}
export default Sample;
부모로부터 props를 전달받은 Sample component는 import PropTypes from 'prop-types';
구문을 통해 PropTypes
에 있는 메서드들을 활용, 다양한 설정을 해줄 수 있다.
우선 static defaultProps
를 정의함으로써, 혹시나 주어지지 않은 props에 대해 기본값을 설정해줄 수 있다.
static propTypes
를 통해 다양한 옵션들을 지정해줄 수 있다. 여기에는 전달받은 prop의 type 혹은 꼭 주어져야하는지 여부(isRequired) 등이 기록되게 된다.
const { color, idNumber, children, ungivenProp } = this.props;
구문을 통해 props를 render 메서드 내부에서 더 편하게 쓸 수 있도록 비구조화 할당destructuring assignment 해준다. 해당 props들은 {propName}
형태로 return
문 내의 template에 활용하면 된다.
위의 형식 말고도 아래와 같이 위의 코드를 다시 써볼 수 있다. 다만 props 관련 설정들이 component 아래에 위치하게 되어 논리적인 흐름과 가시적인 코드가 다르다보니, 개인적으로는 선호하지 않는다.
(src/Sample.js)
import { Component } from 'react';
import PropTypes from 'prop-types';
class Sample extends Component {
render() {
const { color, idNumber, children, ungivenProp } = this.props;
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>Props part</h2>
<p style={style}>And our parent gives us {color} props that defines text color of this sentence.</p>
<p>Also children value: {children}</p>
<p>If some prop is required, and when it's not given, there will be error in console. : {idNumber}</p>
<p>If there is un given prop, you can give default value({ungivenProp}) to that prop.</p>
</div>
</>
);
}
}
Sample.defaultProps = {
ungivenProp: 'Default Value'
}
Sample.propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
export default Sample;
역시 필요한 부분만 잘라서 보자. App.js에도 state가 있었지만, Sample.js만 보아도 충분하여 Sample.js만 살펴본다.
(src/Sample.js)
import { Component } from 'react';
class Sample extends Component {
state = {
number: 0,
message: '',
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'},
],
nextId: 6,
nameInput: ''
}
handleClick = () => {
this.setState((prevState, props) => {
return {number: prevState.number+1}
}, () => console.log('first'));
this.setState(prevState => ({
number: prevState.number+1
}), () => console.log('second'));
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
handleKeyPress = (e) => {
if(e.key === 'Enter'){
this.setState({
[e.target.name]: ''
});
}
}
handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
this.setState({
names: [
...this.state.names,
{ id: this.state.nextId, text: this.state.nameInput }
],
nextId: this.state.nextId + 1,
[e.target.name]: '',
});
}
}
handleDoubleClickFilter = (id) => {
const nextNames = this.state.names.filter(name => name.id !== id);
this.setState({
names: nextNames
});
}
render() {
const name = 'Jinwoo';
const namesList = this.state.names.map(name => <li key={name.id} onDoubleClick={() => this.handleDoubleClickFilter(name.id)}>{name.text}</li>)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<p>You are {name}, right?</p>
</div>
<div>
<h2>State and Event part</h2>
<p>Changing Number: {this.state.number}</p>
<button
className="number"
onClick={this.handleClick}
>increase +2</button>
<input type="text" name="message"
value={this.state.message}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={this.state.nameInput}
onChange={this.handleChange}
onKeyPress={this.handleKeyPressMap}
/>
</div>
</>
)
}
}
export default Sample;
변하지 않는 값을 굳이 state에서 관리할 필요는 없다. render 함수 내 최상단에 const name = 'Jinwoo';
와 같이 써준 뒤 활용해줄 수 있다.
우선 해당 component에서 변화할 가능성이 있는 데이터들(여기서는 number
, message
, names
, nextId
, nameInput
)을 state 안에 담는다.
state 담긴 것들은 this.setState
메서드로 변경해줄 수 있다. 사실 handleClick
함수는 아래와 같이 더 간단하게 표현될 수 있다.
handleClick = () => {
this.setState({number: this.state.number+2});
}
위와 같이 표현하면 state 내 다른 것들은 값이 그대로인 채로 number의 값만 변하게 된다.
다만 한 메서드 내에서 변경된 이후의 값에 다시 접근하고 싶다면 prevState, props(그 중에서도 prevState)를 활용해야 하여 굳이 예시를 좀 번거롭게 잡았다.
handleClick = () => {
this.setState((prevState, props) => {
return {number: prevState.number+1}
}, () => console.log('first'));
this.setState(prevState => ({
number: prevState.number+1
}), () => console.log('second'));
}
위의 코드를 보면 우선 prevState는 호출된 순간의 state 상태를 나타낸다. props는 현재 가지고 있는 props를 가리킨다. props가 불필요한 경우(사실 두 this.setState
모두 필요한 경우는 아니다) 두 번째 parameter인 props의 생략이 가능하다. 여기서는 각각 한 번씩 number를 증가시켜줌으로써 결과적으로 한 번의 클릭마다 state 내의 number 상태가 +2가 되는 것을 알 수 있다.
위와 같이 this.setState
의 두 번째 인자로 callback function을 지정해줄 수 있다.
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
재사용성을 위해 key값을 []
로 감싸 표현해주는 것이 일반적이다. 위와 같은 코드가 예시에서처럼 name="message"
가 선언된 input
tag 내의 onChange property에 선언되면, input 값이 변화할 때마다 state 내의 message가 변하고, 그것이 브라우저에 표시될 것이다.
constructor lifecycle을 재선언 해줌으로써 아래와 같이도 코드를 변환할 수 있다. 다만 익숙하지 않아 개인적으로는 선호하지 않는다.
(src/Sample.js)
import { Component } from 'react';
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
number: 0,
message: '',
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'},
],
nextId: 6,
nameInput: ''
}
}
(...)
event에서 활용되고 있는 parameter인 e
는 syntheticEvent로, 웹 브라우저 내의 native event를 감싸고 있는 객체다. 다만 한 가지 다른 점은 이벤트가 끝난 뒤 초기화되다보니 비동기적으로 e
를 참조할 수 없다. 그럴 때에는 syntheticEvent 객체 내의 persist()
메서드를 활용해주어야 한다.
(src/Sample.js)
import { Component } from 'react';
class Sample extends Component {
(...)
render() {
const name = 'Jinwoo';
const namesList = this.state.names.map(name => <li key={name.id} onDoubleClick={() => this.handleDoubleClickFilter(name.id)}>{name.text}</li>)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<p>You are {name}, right?</p>
</div>
<div>
<h2>State and Event part</h2>
<p>Changing Number: {this.state.number}</p>
<button
className="number"
onClick={this.handleClick}
>increase +2</button>
<input type="text" name="message"
value={this.state.message}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={this.state.nameInput}
onChange={this.handleChange}
onKeyPress={this.handleKeyPressMap}
/>
</div>
</>
)
}
}
export default Sample;
onChange
, onKeyPress
등 camel case 방식으로 선언해주고 있음을 볼 수 있다.
this.handleClick
과 같이 this를 붙여주는 것을 잊지 말자. javascript를 이미 다루어보았으면 알겠지만, e => this.handleKeyPress(e)
처럼 적지 않고 this.handleKeyPress
만 적어도 알아서 syntheticEvent 객체가 parameter에 전달된다.
이 정도만 유의해도 event에서 어려움을 겪는 일은 흔치 않을 것이다. event 함수 선언 역시 아래와 같이 constructor lifecycle을 활용해 진행할 수도 있지만, 역시나 번거로워서 나는 잘 이용하지 않는 방식이다.
(src/Sample.js)
import { Component } from 'react';
class Sample extends Component {
state = {
number: 0,
message: '',
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'},
],
nextId: 6,
nameInput: ''
}
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyPressMap = this.handleKeyPressMap.bind(this);
this.handleDoubleClickFilter = this.handleDoubleClickFilter.bind(this);
}
handleClick() {
this.setState((prevState, props) => {
return {number: prevState.number+1}
}, () => console.log('first'));
this.setState(prevState => ({
number: prevState.number+1
}), () => console.log('second'));
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value
});
}
handleKeyPress(e) {
if(e.key === 'Enter'){
this.setState({
[e.target.name]: ''
});
}
}
handleKeyPressMap(e) {
if(e.key === 'Enter'){
this.setState({
names: [
...this.state.names,
{ id: this.state.nextId, text: this.state.nameInput }
],
nextId: this.state.nextId + 1,
[e.target.name]: ''
});
}
}
handleDoubleClickFilter(id) {
const nextNames = this.state.names.filter(name => name.id !== id);
this.setState({
names: nextNames
});
}
(...)
export default Sample;
ref는 DOM을 꼭 건드려야만 할 수 있는 일이 존재할 때 특수하게 사용하게 된다. Component에도 ref를 줄 수 있는데, 그런 경우는 보통 상위 Component에서 자식 Component를 (자식 component의 메서드를 활용해서) 조작하고 싶을 때 사용한다. 물론 여기서 볼 예시는 Component에 ref를 준 케이스 말고, DOM element에 focus를 주기 위해서 ref를 활용하는 예시다.
역시나 필요한 코드만 간략하게 살펴보자.
(src/Sample.js)
import { Component } from 'react';
class Sample extends Component {
handleClickRef = () => {
this.input.focus();
}
render() {
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>Ref part</h2>
<input type="text"
ref={ref => this.input=ref}
/>
<button
onClick={this.handleClickRef}
>
Focus to Input
</button>
</div>
</>
);
}
}
export default Sample;
input
자리에는 다른 변수명이 들어가도 상관 없다.
아래와 같이 다시 쓸 수도 있다. React 16.3 version부터 도입되었다.
import { Component } from 'react';
class Sample extends Component {
input = React.createRef();
handleClickRef = () => {
this.input.current.focus();
}
render() {
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>Ref part</h2>
<input type="text"
ref={this.input}
/>
<button
onClick={this.handleClickRef}
>
Focus to Input
</button>
</div>
</>
);
}
}
export default Sample;
current
의 존재에 유의하자.
this.input
를 사용하여 불러온 DOM 요소에 직접 조작을 가한다. 여기서는 focus 메서드를 사용해 버튼을 클릭하면 input에 포커스가 가도록 했다.
import { Component } from 'react';
class Sample extends Component {
state = {
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'},
],
nextId: 6,
nameInput: ''
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
this.setState({
names: [
...this.state.names,
{ id: this.state.nextId, text: this.state.nameInput }
],
nextId: this.state.nextId + 1,
[e.target.name]: ''
});
}
}
handleDoubleClickFilter = (id) => {
const nextNames = this.state.names.filter(name => name.id !== id);
this.setState({
names: nextNames
});
}
render() {
const namesList = this.state.names.map(name => <li key={name.id} onDoubleClick={() => this.handleDoubleClickFilter(name.id)}>{name.text}</li>)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={this.state.nameInput}
onChange={this.handleChange}
onKeyPress={this.handleKeyPressMap}
/>
</div>
</>
)
}
}
export default Sample;
javascript의 내장 메서드 map을 활용해서 반복적인 요소를 쉽게 구현할 수 있다. jsx 문법에서만 가능한 코드의 모습이다. 또한 filter 함수를 이용해 조작하고 싶은 DOM 요소를 key 값을 활용해 콕 찝을 수 있다. 조금 길지만 쭉 훑어보면 어렵지 않은 내용이라 이 이상의 설명은 생략하도록 한다.
map을 통해 element를 반복적으로 만들어줄 때, key를 선언해야 한다. key는 언제나 유일한 값을 지녀야 한다. key는 virtual DOM을 비교하는 과정에서 이를 훨씬 수월하고 신속하게 해낼 수 있도록 도와주는 역할을 한다. 이를 설정하지 않는다면 console 창에 경고 메시지가 표시된다.
참고 서적 :
김민준, 2019, 길벗, 『리액트를 다루는 기술, 개정판』