리액트
#5. 프로퍼티, 상태, 컴포넌트 트리
YJH3968
2021. 3. 29. 14:53
728x90
1. 프로퍼티 검증
- javascript는 type 검증이 느슨한 언어이기 때문에 변수값의 데이터 type이 바뀔 수 있다. 이로 인해 애플리케이션을 디버깅할 때 시간이 아주 오래 걸릴 수 있다.
- 리액트 component는 프로퍼티 type을 지정하고 검증하는 방법을 제공한다. 이를 통해 디버깅 시간을 크게 줄일 수 있고, 프로퍼티에 잘못된 type의 값을 지정하면 경고가 표시되기 때문에 버그를 쉽게 찾을 수 있다.
- 리액트에는 자동 프로퍼티 검증 기능이 있다. 이 검증 기능은 React.ProtoTypes를 통해 구현할 수 있다.
// prop-types 모듈 설치
npm install prop-types --save
타입 | 검증기 |
Arrays | React.ProtoTypes.array |
Boolean | React.ProtoTypes.bool |
Functions | React.ProtoTypes.func |
Numbers | React.ProtoTypes.number |
Objects | React.ProtoTypes.object |
Strings | React.ProtoTypes.string |
- 이 절에서는 조리법 앱의 Summary component를 만든다. Summary component는 조리법 제목과 재료 수, 조리 절차의 단계 수를 함께 표시한다.
- 이때 component 프로퍼티의 type을 검증하는 게 중요해지는데, 만약 Summary component를 다음과 같이 구현했다고 하자.
const Summary = createClass({
displayName: "Summary",
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients.length} 종류 | </span>
<span>총 {steps.length} 단계 </span>
</p>
</div>
)
}
})
- ingredients와 steps가 배열인 경우에는 큰 문제가 없지만 만약 배열이 아닌 문자열을 넘긴다면 문자열의 길이를 출력할 것이다.
- 이러한 문제를 해결하기 위해 createClass의 인자 객체에 propTypes라는 프로퍼티를 추가해 프로퍼티 type 검증을 사용한다.
// 리액트의 내장 프로퍼티 type 검증을 사용한 Summary
const Summary = createClass({
displayName: "Summary",
propTypes: {
ingredients: PropTypes.array,
steps: PropTypes.array,
title: PropTypes.string
},
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients.length} 종류 | </span>
<span>총 {steps.length} 단계 </span>
</p>
</div>
)
}
})
- 이렇게 코드를 작성한 경우 ingredients, steps에 다른 type의 값을 넣으면 오류를 발생시킨다.
- 프로퍼티를 지정하지 않고 Summary component를 렌더링하면 프로퍼티의 type이 undefined가 되어 오류가 발생한다. 리액트는 이를 방지하기 위해 필수 프로퍼티를 지정하는 방법을 제공한다.
// 필수 프로퍼티를 지정한 Summary
const Summary = createClass({
displayName: "Summary",
propTypes: {
ingredients: PropTypes.array.isRequired,
steps: PropTypes.array.isRequired,
title: PropTypes.string
},
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients.length} 종류 | </span>
<span>총 {steps.length} 단계 </span>
</p>
</div>
)
}
})
- 이렇게 코드를 작성하면 아무 프로퍼티도 지정하지 않고 Summary component를 렌더링하면 리액트는 오류가 발생하기 전에 콘솔에 경고를 표시해서 문제가 있다는 사실을 알려준다. 따라서 좀 더 쉽게 잘못된 부분을 찾을 수 있다.
- component에서 사용하는 배열의 용도가 단지 배열의 length 프로퍼티뿐인 경우, 배열 대신 수를 프로퍼티로 받아들이도록 변경하는 편이 더 낫다.
const Summary = createClass({
displayName: "Summary",
propTypes: {
ingredients: PropTypes.number.isRequired,
steps: PropTypes.number.isRequired,
title: PropTypes.string
},
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients} 종류 | </span>
<span>총 {steps} 단계 </span>
</p>
</div>
)
}
})
- 프로퍼티에 대해 default 값을 지정할 수 있다. 이를 이용해 component를 더 유연하게 만들 수 있고 사용자가 실수로 프로퍼티 중 일부를 지정하지 않았을 때 발생하는 오류를 방지할 수 있다.
const Summary = createClass({
displayName: "Summary",
propTypes: {
ingredients: PropTypes.number,
steps: PropTypes.number,
title: PropTypes.string
},
getDefaultProps() {
return {
ingredients: 0,
steps: 0,
title: "[무제]"
}
},
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients} 종류 | </span>
<span>총 {steps} 단계 </span>
</p>
</div>
)
}
})
- 프로퍼티의 type에 더해 좀 더 자세한 검증이 필요한 경우 커스텀 검증기를 추가할 수 있다.
- 리액트의 커스텀 검증기는 함수로 구현하는데, 검증 요구사항을 만족하지 않는 경우 에러를 반환하고 요구사항을 만족하는 경우 null을 반환한다.
// 커스텀 검증기 예제
propTypes: {
ingredients: PropTypes.number,
steps: PropTypes.number,
title: (props, propName) => // 이 예시에서 propName은 'title'이다.
(typeof props[propName] !== 'string') ?
new Error("제목(title)은 문자열이어야 합니다.") :
(props[propName].length > 20) ?
new Error("제목은 20자 이내여야 합니다.") :
null
}
- 커스텀 검증기를 구현할 때는 프로퍼티를 콜백 함수로 설정해야 한다. component를 렌더링할 때 리액트는 이 함수에 props 프로퍼티 객체와 프로퍼티 이름을 인자로 넘긴다.
- ES6 클래스를 사용할 경우 프로퍼티 검증 기능을 추가할 때 클래스 본문 외부에서 클래스 인스턴스에 대해 이루어져야 한다.
const Summary extends React.Component {
render() {
const {ingredients, steps, title} = this.props
return (
<div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients} 종류 | </span>
<span>총 {steps} 단계 </span>
</p>
</div>
)
}
}
Summary.propTypes = {
ingredients: PropTypes.number,
steps: PropTypes.number,
title: (props, propName) => // 이 예시에서 propName은 'title'이다.
(typeof props[propName] !== 'string') ?
new Error("제목(title)은 문자열이어야 합니다.") :
(props[propName].length > 20) ?
new Error("제목은 20자 이내여야 합니다.") :
null
}
Summary.defaultProps = {
ingredients: 0,
steps: 0,
title: "[무제]"
}
- 상태가 없는 함수형 component에도 propTypes와 defaultProps 객체 리터럴을 추가할 수 있다. defaultProps 객체 리터럴은 함수 인자 부분에 default 인자를 지정하는 것으로 대체할 수 있다.
const Summary = ({ ingredients=0, steps=0, title=`[무제]` }) => {
return <div className="summary">
<h1>{title}</h1>
<p>
<span>재료 {ingredients} 종류 |</span>
<span>총 {steps} 단계 </span>
</p>
</div>
}
Summary.propTypes = {
ingredients: React.PropTypes.number,
steps: React.PropTypes.number,
title: (props, propName) => // 이 예시에서 propName은 'title'이다.
(typeof props[propName] !== 'string') ?
new Error("제목(title)은 문자열이어야 합니다.") :
(props[propName].length > 20) ?
new Error("제목은 20자 이내여야 합니다.") :
null
}
2. 참조
- 리액트 component가 자식 element와 상호작용할 때 필요한 기능으로 사용자 입력을 받는 UI element와 상호작용할 때 자주 사용한다.
// submit method를 추가해 자식 element와 상호작용하는 예제
import { Component } from 'react'
import { render } from 'react-dom'
class AddColorForm extends Component {
constructor(props) {
super(props)
this.submit = this.submit.bind(this)
}
submit(e) {
const { _title, _color } = this.refs
e.preventDefault();
alert(`새로운 색: ${_title.value} ${_color.value}`)
_title.value='';
_color.value='#000000';
_title.focus();
}
render() {
return (
<form onSubmit={this.submit}>
<input ref="_title" type="text" placeholder="색 이름..." required />
<input ref="_color" type="color" required />
<button>추가</button>
</form>
)
}
}
- submit을 method로 옮겼기 때문에 ES6 component에 생성자를 추가하고 component가 사용하는 영역에서 this를 사용해 method를 직접 바인딩해야 한다.
- render method에서는 form의 onSubmit 핸들러를 component의 submit method로 지정한다. 참조하고 싶은 component에는 ref 필드를 추가함으로써 리액트가 DOM element를 참조할 때 this.refs를 통해 식별자로 사용할 수 있다.
- 리액트 component에서 데이터를 수집하는 일반적인 방법은 역방향 데이터 흐름을 사용하는 것이다.
- 역방향 데이터 흐름이란 component가 데이터를 돌려줄 때 사용할 수 있는 콜백 함수를 component의 프로퍼티로 설정하는 것을 말한다. 즉, 함수를 component에 프로퍼티로 넘기고 component는 데이터를 그 함수의 인자로 돌려준다.
// 역방향 데이터 흐름 예제
const logColor = (title, color) =>
console.log(`새로운 색: ${title} | ${value}`)
<AddColorForm onNewColor={logColor} />
// AddColorForm의 submit method
submit() {
const {_title, _color} = this.refs
this.props.onNewColor(_title.value, _color.value)
_title.value=''
_color.value='#000000'
_title.focus()
}
- AddColorForm component는 데이터를 모아서 전달한다. AddColorForm을 사용해서 사용자로부터 색 정보를 얻어 이를 다른 component에 전달하거나 수집한 데이터를 처리하는 method에 넘길 수 있다.
- 양방향 데이터 바인딩을 선택적으로 만들기 위해서는 콜백 프로퍼티를 호출하기 전에 그 프로퍼티에 함수가 지정되어 있는지 검사해야 한다.
// 선택적 함수 프로퍼티 예제
if (this.props.onNewColor) {
this.props.onNewColor(_title.value, _color.value)
}
// component의 propTypes와 defaultProps에 함수 프로퍼티를 지정하는 방법도 있다.
AddColorForm.propTypes {
onNewColor: Proptypes.func
}
AddColorForm.defaultProps {
onNewColor: f=>f // 이 화살표 함수는 단순히 자신이 받은 첫 번째 인자를 다시 반환한다.
}
- 상태가 없는 함수형 component에서는 참조를 사용할 경우 this가 없기 때문에 ref attribute를 문자열로 지정하는 대신 함수를 사용한다. 이 함수는 input element 인스턴스를 인자로 받기 때문에 이를 로컬 변수에 저장할 수 있다.
// 함수형 component 참조 예제
const AddColorForm = ({onNewColor=f=>f}) => {
let _title, _color
const submit = e => {
e.preventDefault()
onNewColor(_title.value, _color.value)
_title.value=''
_color.value='#000000'
_title.focus()
}
return (
<form onSubmit={submit}>
<input ref={input => _title=input} type="text" placeholder="색 이름.." required/>
<input ref={input => _color=input} type="color" required/>
<button>추가</button>
</form>
)
}
3. 리액트 상태 관리
- 지금까지 리액트 component는 데이터를 처리하기 위해 프로퍼티만 사용했으나 프로퍼티는 변경 불가능하기 때문에 렌더링하고 나면 UI를 바꾸기 위해서는 component tree를 새로운 프로퍼티를 새로 그려주는 다른 매커니즘이 필요하다.
- 리액트 상태는 component 안에서 바뀌는 데이터를 관리하기 위해 리액트가 기본으로 제공하는 기능이다. 애플리케이션 상태가 바뀌면 UI는 자동으로 그 상태를 반영해 새로 렌더링된다.
// 별점을 표시하는 StarRating 예제
const Star = ({ selected=false, onClick=f=>f }) =>
<div className={(selected) ? "star selected" : "star"} onClick={onClick}></div>
Star.propTypes = {
selected: PropTypes.bool,
onClick: PropTypes.func
}
// star css
.star {
cursor: pointer;
height: 25px;
width: 25px;
margin: 2px;
float: left;
background-color: grey;
clip-path: polygon(
50% 0%,
63% 38%,
100% 38%,
69% 59%,
82% 100%,
50% 75%,
18% 100%,
31% 59%,
0% 38%,
37% 38%
);
}
.star .selected {
background-color: red;
}
- Star는 상태가 없는 함수형 component다. 즉, 상태를 가질 수 없다. 상태가 없는 함수형 component는 더 복잡한 상태가 있는 component의 자식 역할을 하도록 만든 component다.
- Star component를 이용해 평점 정보를 상태에 저장하는 StarRating component를 만든다.
// createClass에서 상태를 나타내는 예제
const StarRating = createClass({
displayName: 'StarRating',
propTypes: {
totalStars: PropTypes.number
},
getDefaultProps() {
return {
totalStars: 5
}
},
getInitialState() { // 상태 초기화
return {
starsSelected: 0
}
},
change(starsSelected) {
this.setState({starsSelected})
},
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) => // 길이가 totalStars인 Star element의 배열을 만든다.
<Star key={i} selected={i<starsSelected} onClick={() => this.change(i+1)} />
)} // Star element의 인덱스에 1을 더한 값을 change method에 전달하는 이유는
// 배열의 인덱스가 0부터 시작하기 때문이다.
<p>별점: {starsSelected} / {totalStars}</p>
</div>
)
}
})
// ES6 class에서 상태를 나타내는 예제
class StarRating extends Component {
constructor(props) {
super(props)
this.state = {
starsSelected: 0
}
this.change = this.change.bind(this)
}
change(startsSelected) {
this.setState({startsSelected})
}
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i} selected={i<starsSelected} onClick={() => this.change(i+1)} />
)}
<p>별점: {starsSelected} / {totalStars}</p>
</div>
)
}
}
StarRating.propTypes = {
totalStars: PropTypes.number
}
StarRating.defaultProps = {
totalStars: 5
}
- ES6 component를 마운트할 때 그 component의 생성자가 호출되면서 프로퍼티가 첫 번째 인자로 전달된다. 이 프로퍼티는 다시 super 호출에 의해 상위 클래스인 React.Component에 전달돼 상태를 관리해주는 기능을 이용해 인스턴스를 꾸며준다.
- component의 입력 프로퍼티로부터 상태 변수를 초기화할 수 있다. 이에는 componentWillMount라는 method를 이용하는데, 이 method는 component가 마운트될 때 단 한 번 호출된다. this.setState()를 이 method에서 호출할 수 있고, this.props를 사용할 수 있다.
// componentWillMount를 추가한 예제
const StarRating = createClass({
displayName: 'StarRating',
propTypes: {
totalStars: PropTypes.number
},
getDefaultProps() {
return {
totalStars: 5
}
},
getInitialState() { // 상태 초기화
return {
starsSelected: 0
}
},
componentWillMount() {
const { starsSelected } = this.props
if (starsSelected) {
this.setState({starsSelected})
}
},
change(starsSelected) {
this.setState({starsSelected})
},
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i} selected={i<starsSelected} onClick={() => this.change(i+1)} />
)}
<p>별점: {starsSelected} / {totalStars}</p>
</div>
)
}
})
render(
<StarRating totalStars={7} starsSelected={3} />,
document.getElementById('react-container')
)
// ES6 클래스 component를 사용하는 경우 더 쉽게 상태를 초기화할 수 있다.
constructor(props) {
super(props)
this.state = {
starsSelected: props.starsSelected || 0
}
this.change = this.chage.bind(this)
}
- 꼭 필요한 경우에만 상태 변수를 프로퍼티로부터 설정하는 것이 좋다. 리액트 component를 다룰 때는 상태가 있는 component의 수를 최소화해야 하기 때문이다.
4. component tree 안의 상태
- 대부분의 리액트 애플리케이션은 상태 데이터를 루트 component에 모아둘 수 있다. 상태 데이터를 프로퍼티를 통해 component tree의 아랫방향으로 전달할 수 있고, 양방향 함수 바인딩을 활용해 tree 아래쪽에서 수집한 데이터를 다시 올려 보낼 수 있다.
- 그 결과 애플리케이션 전체의 상태 데이터가 루트 component 한 곳에 존재하게 된다.
// 색 관리 앱의 데이터
{
colors: [
{
"id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
"title": "해질녘 바다",
"color": "#00c4e2",
"rating": "5"
},
{
"id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
"title": "잔디",
"color": "#26ac56",
"rating": 3
},
{
"id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
"title": "밝은 빨강",
"color": "#ff0000",
"rating": 0
}
]
}
- 앞에서 만든 StarRating component는 표현 component로 다루고 상태가 없는 함수형 component로 선언하는 편이 더 타당하다.
- 표현 component란 애플리케이션의 여러 부분을 표시하는 데만 관심이 있는 component다. 즉, DOM element나 다른 표현 component를 렌더링하는 일만 담당한다.
- 표현 component에 데이터를 전달할 때는 프로퍼티로 전달하고, 데이터를 받고 싶을 때는 콜백함수를 이용한다.
// 상태가 없는 함수형 component의 형태로 바꾼 StarRating
const StarRating = ({starsSelected=0, totalStars=5, onRate=f=>f}) =>
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i} selected={i<starsSelected} onClick={()=>onRate(i+1)}/>
)}
<p>별점: {starsSelected} / {totalStars}</p>
</div>
- 사용자가 평점을 변경하면 setState를 호출하는 대신 onRate를 호출하면서 변경된 평점을 인자로 전달한다.
- 상태를 루트 component 한 곳에만 두도록 제약을 가하고 상태 데이터를 모든 자식 component에 프로퍼티로 전달한다.
- 색 관리 앱에서 상태는 App component에 정의된 색의 배열로 이루어진다. ColorList component에 이 색 정보를 전달할 때는 프로퍼티를 사용한다.
// 루트 component App
class App extend Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
}
render() {
const { colors } = this.state
return (
<div className="app">
<AddColorForm />
<ColorList colors={colors} />
</div>
)
}
}
// ColorList component
const ColorList = ({ colors=[] }) =>
<div className="color-list">
{(colors.length === 0) ?
<p>색이 없습니다. (색을 추가해주세요)</p> :
colors.map(color =>
<Color key={color.id} {...color} />
)
}
</div>
// Color component
const Color = ({ title, color, rating=0 }) =>
<section className="color">
<h1>{title}</h1>
<div className="color" style={{ backgroundColor: color }}></div>
<div>
<StarRating starsSelected={rating} />
</div>
</section>
- 위 코드와 같이 루트 component에서 받은 데이터를 자식 component에 순차적으로 전달한다.
- 사용자가 UI에서 무언가를 변경하면 그 입력이 component tree를 타고 올라가 App component에 전달되고 App component에 있는 앱의 상태가 바뀐다. 이런 과정은 콜백 함수 프로퍼티를 통해 이루어진다.
- 새 색을 추가하려면 각 색을 유일하게 식별할 수 있어야 한다. 이를 위해 고유 ID를 만들 때 uuid 라이브러리를 사용한다.
// uuid 라이브러리 설치
npm install uuid --save
// 색 추가 기능을 더한 루트 component App
import { Component } from 'react'
import { v4 } from 'uuid'
import AddColorForm from './AddColorForm'
import ColorList from './ColorList'
export class App extend Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
this.addColor = this.addColor.bind(this)
}
addColor(title, color) {
const colors = [
...this.state.colors,
{
id: v4(),
title,
color,
rating: 0
}
]
this.setState({colors})
}
render() {
const { addColor } = this
const { colors } = this.state
return (
<div className="app">
<AddColorForm onNewColor={addColor}/>
<ColorList colors={colors} />
</div>
)
}
}
- setState가 호출될 때마다 render method가 호출된다. 갱신된 색 데이터는 렌더링을 하면서 UI를 구축하는 과정에서 프로퍼티를 component tree의 아랫방향으로 전달된다.
- 이제 component에서 데이터가 바뀌었을 때 루트 component로 콜백함수를 이용해 바뀐 데이터를 전달하는 코드를 추가하면 다음과 같다.
// 색의 평점을 지정하거나 색을 제거하는 기능을 추가한 Color component
const Color = ({ title, color, rating=0, onRemove=f=>f, onRate=f=>f }) =>
<section className="color">
<h1>{title}</h1>
<button onClick={onRemove}>X</button>
<div className="color" style={{ backgroundColor: color }}></div>
<div>
<StarRating starsSelected={rating} onRate={onRate} />
</div>
</section>
// 위 Color component를 반영한 ColorList component
const ColorList = ({ colors=[], onRate=f=>f, onRemove=f=>f }) =>
<div className="color-list">
{(colors.length === 0) ?
<p>색이 없습니다. (색을 추가해주세요)</p> :
colors.map(color =>
<Color key={color.id} {...color}
onRate={(rating) => onRate(color.id, rating)}
onRemove={() => onRemove(color.id)} />
)
}
</div>
// 위 ColorList를 반양한 App component
class App extend Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
this.addColor = this.addColor.bind(this)
this.rateColor = this.rateColor.bind(this)
this.removeColor = this.removeColor.bind(this)
}
addColor(title, color) {
const colors = [
...this.state.colors,
{
id: v4(),
title,
color,
rating: 0
}
]
this.setState({colors})
}
rateColor(id, rating) {
const colors = this.state.colors.map(color =>
(color.id !== id) ?
color :
{
...color,
rating
}
)
this.setState({colors})
}
removeColor(id) {
const colors = this.state.colors.filter(
color => color.id !== id
)
this.setState({colors})
}
render() {
const { addColor, rateColor, removeColor } = this
const { colors } = this.state
return (
<div className="app">
<AddColorForm onNewColor={addColor}/>
<ColorList colors={colors} onRate={rateColor} onRemove={removeColor}/>
</div>
)
}
}
출처 : learning react(2018)
728x90