First written: 22-12-17
Uploaded: 22-12-28
Last modified: 23-03-01
이번 설명에서 App.js는 거의 등장하지 않을 계획이므로, App.js는 굳이 함수화하지 않았다.
(src/App.js)
import { Component } from 'react';
import SampleFunctional from './SampleFunctional';
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>
<SampleFunctional color={this.state.color} idNumber={1}>SampleFunctional</SampleFunctional>
</div>
);
}
}
export default App;
(src/SampleFunctional.js)
import { useState } from 'react';
import PropTypes from 'prop-types';
const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
const [number, setNumber] = useState(0);
const [message, setMessage] = useState('');
const [names, setNames] = useState({
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'}
],
nextId: 6,
nameInput: ''
});
const handleClick = () => {
setNumber((prevState, props) => {
return prevState+1;
});
setNumber(prevState => prevState+1);
}
const handleChange = (e) => {
if(e.target.name === 'message') setMessage(e.target.value);
else if(e.target.name === 'nameInput') setNames({...names, nameInput: e.target.value})
}
const handleKeyPress = (e) => {
if(e.key === 'Enter'){
setMessage('');
}
}
const handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
setNames({...names,
names: [
...names.names,
{ id: names.nextId, text: names.nameInput }
],
nextId: names.nextId + 1,
nameInput: '',
})
}
}
const handleDoubleClickFilter = (id) => {
const nextNames = names.names.filter(name => name.names.id !== id);
setNames({
...names,
names: nextNames
});
}
const name = 'Jinwoo';
const style = {
color: color
};
const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => 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: {number}</p>
<button
className="number"
onClick={handleClick}
>increase +2</button>
<input type="text" name="message"
value={message}
onChange={handleChange}
onKeyPress={handleKeyPress}
/>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={names.nameInput}
onChange={handleChange}
onKeyPress={handleKeyPressMap}
/>
</div>
</>
);
}
SampleFunctional.defaultProps = {
ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
export default SampleFunctional;
이전 class component 글을 본 사람이라면 이후의 설명에서 같은 말이 반복되는 것을 알 수 있을 것이다. 코드만 바뀌었지 설명이 동일한 부분들이 많아서 그대로 두었다. 원래는 아주 간단한 설명만 남기려고 하다가 혹시나 functional component만 보는 사람도 있을 수 있어 이전 class component에서 썼던 것들을 재사용했다.
지나치게 긴 위의 코드를 필요한 부분만 잘라서 보자.
(src/App.js)
import { Component } from 'react';
import SampleFunctional from './SampleFunctional';
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>
<SampleFunctional color={this.state.color} idNumber={1}>SampleFunctional</SampleFunctional>
</div>
);
}
}
export default App;
(src/SampleFunctional.js)
import PropTypes from 'prop-types';
const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
const style = {
color: color
};
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>
</>
);
}
SampleFunctional.defaultProps = {
ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
export default SampleFunctional;
Sample component의 상위 component인 App에서 color, idNumber 그리고 children props를 Sample component에 전달하고 있다. children prop은 <Sample>
태그 사이에 쓰여있는 text, 여기서는 SampleFunctional
을 뜻한다.
이를 전달받은 Sample component는 import PropTypes from 'prop-types';
구문을 통해 PropTypes
에 있는 메서드들을 활용, 다양한 설정을 해줄 수 있다.
(src/SampleFunctional.js)
import PropTypes from 'prop-types';
const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
(...)
}
SampleFunctional.defaultProps = {
ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
name: PropTypes.string,
idNumber: PropTypes.number.isRequired
}
export default SampleFunctional;
우선 Component.defaultProps
를 정의함으로써, 혹시나 주어지지 않은 props에 대해 기본값을 설정해줄 수 있다.
Component.propTypes
를 통해 다양한 옵션들을 지정해줄 수 있다. 여기에는 전달받은 prop의 type 혹은 꼭 주어져야하는지 여부(isRequired) 등이 기록되게 된다.
Functional component의 인자로 받아온 { color, idNumber, children, ungivenProp }
를 통해 props를 가져온다. 해당 props들은 {prop}
형태로 return
문 내의 template에 활용하면 된다.
위의 형식 말고도 아래와 같이 위의 코드를 다시 써볼 수 있다. Destructuring assignment비구조화 할당을 통해 this.props로부터 부모가 전해준 props들을 전달받는다. 개인적으로는 약간 더 품이 듦으로 선호하지 않는다.
import PropTypes from 'prop-types';
const SampleFunctional = (props) => {
const { color, idNumber, children, ungivenProp } = props;
(...)
}
역시 필요한 부분만 잘라서 보자.
(src/SampleFunctional.js)
import { useState } from 'react';
const SampleFunctional = () => {
const [number, setNumber] = useState(0);
const [message, setMessage] = useState('');
const [names, setNames] = useState({
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'}
],
nextId: 6,
nameInput: ''
});
const handleClick = () => {
setNumber((prevState, props) => {
return prevState+1;
});
setNumber(prevState => prevState+1);
}
const handleChange = (e) => {
if(e.target.name === 'message') setMessage(e.target.value);
else if(e.target.name === 'nameInput') setNames({...names, nameInput: e.target.value})
}
const handleKeyPress = (e) => {
if(e.key === 'Enter'){
setMessage('');
}
}
const handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
setNames({...names,
names: [
...names.names,
{ id: names.nextId, text: names.nameInput }
],
nextId: names.nextId + 1,
nameInput: '',
})
}
}
const handleDoubleClickFilter = (id) => {
const nextNames = names.names.filter(name => name.names.id !== id);
setNames({
...names,
names: nextNames
});
}
const name = 'Jinwoo';
const style = {
color: color
};
const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => handleDoubleClickFilter(name.id)}>{name.text}</li>)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<p>You are {name}, right?</p>
<div>
<h2>State and Event part</h2>
<p>Changing Number: {number}</p>
<button
className="number"
onClick={handleClick}
>increase +2</button>
<input type="text" name="message"
value={message}
onChange={handleChange}
onKeyPress={handleKeyPress}
/>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={names.nameInput}
onChange={handleChange}
onKeyPress={handleKeyPressMap}
/>
</div>
</>
);
}
export default SampleFunctional;
변하지 않는 값을 굳이 state에서 관리할 필요는 없다. Component 함수 내에 const name = 'Jinwoo';
와 같이 써준 뒤 활용해줄 수 있다.
우선 해당 component에서 변화할 가능성이 있는 데이터들을 state 안에 담는다. 여기서는 Class Component와 다르게 import { useState } from 'react';
를 통해 useState
를 가져와서 state를 관리한다. 이는 Hooks의 일종인데, 차차 더 많은 Hooks를 배우게 될 것이다.
const [message, setMessage] = useState('');
Destructuring assignment가 여기서도 활용된다. 첫 번째 변수에는 state, 두 번째 변수에는 state를 수정하는 메서드가 담기게 된다. useState()
메서드 내부에는 initial value를 넣어준다.
두 번째 변수를 활용해 state를 바꿀 때에는 아래와 같이 한다.
setMessage('연습하는 중입니다.');
Class component 때와 다른 점은, 두 번째 인자로 callback function을 줄 수 없다는 점이다. 같은 효과를 내고자 한다면 나중에 배울 useEffect
가 필요하다. 혹시나 class component 때와 비교하고 싶은 분을 위해 두 코드를 모두 첨부한다. 첫번째 것이 functional component이고 두 번재가 class component이다.
const handleClick = () => {
setNumber((prevState, props) => {
return prevState+1;
});
setNumber(prevState => prevState+1);
}
handleClick = () => { // to avoid to use constructor initialization
this.setState((prevState, props) => {
return {number: prevState.number+1}
}, () => console.log('first'));
this.setState(prevState => ({
number: prevState.number+1
}), () => console.log('second'));
}
아래 코드를 보자. 한 메서드 내에서 변경된 이후의 값에 다시 접근하고 싶다면 prevState, props(그 중에서도 prevState)를 활용해야 하여 굳이 예시를 좀 번거롭게 잡았다.
const handleClick = () => {
setNumber((prevState, props) => {
return prevState+1;
});
setNumber(prevState => prevState+1);
}
위의 코드를 보면 우선 prevState는 호출된 순간의 state 상태를 나타낸다. props는 현재 가지고 있는 props를 가리킨다. props가 불필요한 경우(사실 두 setNumber
모두 필요한 경우는 아니다) 두 번째 parameter인 props의 생략이 가능하다. 여기서는 각각 한 번씩 number를 증가시켜줌으로써 결과적으로 한 번의 클릭마다 state 내의 number 상태가 +2가 되는 것을 알 수 있다.
(src/SampleFunctional.js)
import { useState } from 'react';
const SampleFunctional = () => {
(...)
return (
<>
<h1>Hello, it's React.js example</h1>
<div>
<p>You are {name}, right?</p>
<div>
<h2>State and Event part</h2>
<p>Changing Number: {number}</p>
<button
className="number"
onClick={handleClick}
>increase +2</button>
<input type="text" name="message"
value={message}
onChange={handleChange}
onKeyPress={handleKeyPress}
/>
</div>
<div>
<h2>map/filter function</h2>
<ul>
{namesList}
</ul>
<input type="text" name="nameInput"
value={names.nameInput}
onChange={handleChange}
onKeyPress={handleKeyPressMap}
/>
</div>
</>
);
}
export default SampleFunctional;
event에서 활용되고 있는 parameter인 e
는 syntheticEvent로, 웹 브라우저 내의 native event를 감싸고 있는 객체다. 다만 한 가지 다른 점은 이벤트가 끝난 뒤 초기화되다보니 비동기적으로 e
를 참조할 수 없다. 그럴 때에는 syntheticEvent 객체 내의 persist()
메서드를 활용해주어야 한다.
onChange
, onKeyPress
등 camel case 방식으로 선언해주고 있음을 볼 수 있다.
(src/SampleFunctional.js)
import { useState } from 'react';
const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
const [names, setNames] = useState({
names: [
{id: 1, text: 'Smith'},
{id: 2, text: 'Kwang'},
{id: 3, text: 'Leuitong'},
{id: 4, text: 'Umnuwoa'},
{id: 5, text: 'Alejandro'}
],
nextId: 6,
nameInput: ''
});
const handleKeyPressMap = (e) => {
if(e.key === 'Enter'){
setNames({...names,
names: [
...names.names,
{ id: names.nextId, text: names.nameInput }
],
nextId: names.nextId + 1,
nameInput: '',
})
}
}
const handleDoubleClickFilter = (id) => {
const nextNames = names.names.filter(name => name.names.id !== id);
setNames({
...names,
names: nextNames
});
}
const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => 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={names.nameInput}
onChange={handleChange}
onKeyPress={handleKeyPressMap}
/>
</div>
</>
);
}
export default SampleFunctional;
javascript의 내장 메서드 map을 활용해서 반복적인 요소를 쉽게 구현할 수 있다. jsx 문법에서만 가능한 코드의 모습이다. 또한 filter 함수를 이용해 조작하고 싶은 DOM 요소를 key 값을 활용해 콕 찝을 수 있다. 조금 길지만 쭉 훑어보면 어렵지 않은 내용이라 이 이상의 설명은 생략하도록 한다.
map을 통해 element를 반복적으로 만들어줄 때, key를 선언해야 한다. key는 언제나 유일한 값을 지녀야 한다. key는 virtual DOM을 비교하는 과정에서 이를 훨씬 수월하고 신속하게 해낼 수 있도록 도와주는 역할을 한다. 이를 설정하지 않는다면 console 창에 경고 메시지가 표시된다.
참고 서적 :
김민준, 2019, 길벗, 『리액트를 다루는 기술, 개정판』