리액트와 리액트 컴포넌트에서는 데이터를 만들고 다루는 방식 중 하나로 상태(state)를 사용합니다.
데이터를 다루는 또 다른 방식으로는 속성(props) 을 사용합니다.
상태(state)는 컴포넌트가 다루는 값들의 집합체(collection)입니다.
리액트에서는 UI를 state를 다루는 기계장치로 봅니다.
컴포넌트가 setState 함수를 이용해서 state를 변경하게 되면 리액트는 컴포넌트를 다시 랜더링하게 됩니다.
만일 자식 컴포넌트가 속성(props)으로 부모 컴포넌트 상태(state)를 받아서 사용하게 된다면 부모 컴포넌트의 상태(state)가 변경되어 자식 컴포넌트 역시 모두 다시 렌더링 됩니다.
state의 작동방식을 이해하는 것이 중요합니다. state에 의해서 상태를 유지하는 stateful 컴포넌트의 랜더링과 동작이 결정됙 때문입니다.
<속성 초기화로 state 지정하기>
import React from 'react'
class MyComponent extends React.Component{
state={
year: 2016,
name: 'Nader Dabit',
colors: ['blue']
}
render(){
return(
<View>
<Text>My name is: {this.state.name}</Text>
<Text>The year is: {this.state.year}</Text>
<Text>My colors are: {this.state.colors[0]}</Text>
</View>
)
}
}
<생성자로 state 지정하기>
import React, {Component} from 'react'
class MyComponent extends Component{
constructor(){
super()
this.state={
year: 2016,
name: 'Nader Dabit',
colors: ['blue']
}
}
render(){
return(
<View>
<Text>My name is: {this.state.name}</Text>
<Text>The year is: {this.state.year}</Text>
<Text>My colors are: {this.state.colors[0]}</Text>
</View>
)
}
}
상태(state)는 this.setState() 의 호출을 통해서 갱신됩니다.
setState 메서드는 이전 state의 내용과 새로운 state의 내용을 병합하는데, 단순히 새로운 key-value로 이루어진 객체를 전달하면 기존의 state가 유지하던 다른 내용들은 그대로 유지하고, 새로운 내용이 추가됩니다.
대개 render 메서드 앞에 사용자 정의 메서드를 정의하는 것이 좋은데 메서드를 정의하는 순서는 실제 동작에 영향을 미치지 않는다.
<state 갱신하기>
import React, {Component} from 'react'
class MyComponent extends Component{
constructor(){
super()
this.state = {
year: 2018,
}
}
updateYear(){
this.setState({
year: 2019
})
}
render(){
return (
<View>
<Text onPress={() => this.updateYear()}>
The year is: {this.state.year}
</Text>
</View>
)
}
}
state = {
year: 2018
}
⇩
this.setState = ({
year: 2019
})
⇩
state = {
year: 2019
}
setState가 호출될 때마다 리액트는 ( render 메서드를 다시 호출해서) 컴포넌트와 자식 컴포넌트를 다시 렌더링한다.
컴포넌트의 상태(state)를 변경하는 자체가 컴포넌트를 다시 렌더링하는 것을 의미하지는 않음으로 UI상에서의 변화는 일어나지 않는다.
this.setState는 state를 변경하고 다시 render를 호출하게 되는 역할을 수행한다.
➕ forceUpdate
state 자체를 변경하는 경우 리액트에서 강제로 갱신하는 방법
이 메서드를 호출하면 컴포넌트에서 render 메서드를 호출하게 해 UI를 다시 렌더링하게 만든다.
일반적이지도 않고 권장하지도 않는다.
<foreceUpdate로 강제로 다시 렌더링하기>
class MyComponent extends Component{
constructor(){
super()
this.state = {
year: 2018
}
}
updateYear(){
this.state.year = 2019
}
update(){
this.forceUpdate()
}
render(){
return(
<View>
<Text onPress={() => this.updateYear()}>
The year is: {this.state.year}
</Text>
<Text onPress={() => this.update()}>
Force Update
</Text>
</View>
)
}
}
<다양한 데이터 타입으로 상태(state) 지정하기>
class MyComponent extends Component{
constructor(){
super()
this.state = {
year: 2018, //숫자
leapYear: true, //Boolean
topics: ['React', 'React Native', 'JavaScript'], //배열
info:{ //객체
paperback: true,
length: '335 pages'
type: 'programming'
}
}
}
render(){
let leapyear = <Text>This is not a leapyear!</Text>
if(this.state.leapYear){
leapyear = <Text>This is a leapyear!</Text>
}
return (
<View>
<Text>{this.state.year}</Text>
<Text>Length: {this.state.info.length}</Text>
<Text>Type: {this.state.info.type}</Text>
{leafyear}
</View>
)
}
}
props(properties) 는 부모 컴포넌트로부터 전달된 속성값이거나, 컴포넌트가 상속받은 값이다.
최상위에서 선언되고 전달받은 초기값을 변경해야만 props를 변경할 수 있습니다.
<props와 state 간의 대표적인 차이점과 유사점>
props | state |
---|---|
외부 데이터 | 내부 데이터 |
변경 불가능 | 변경 가능 |
부모로부터 상속받는다 | 컴포넌트에서 생성된다. |
부모 컴포넌트가 변경할 수 있다. | 컴포넌트에서만 갱신될 수 있다. |
props로 전달받을 수 있다. | props로 전달받을 수 있다. |
컴포넌트 내부에서 변경할 수 없다. | 컴포넌트 내부에서 변경할 수 있다. |
<book에 대한 값을 선언하고 그 값을 자시 겈ㅁ포넌트에게 정적 props로 전달하는 예>
class MyComponent extends Component{
render(){
return(
<BookDisiplay book="React Native in Action" />
//이렇게도 표현 가능
//<BookDisiplay book={"React Native in Action"} />
)
}
}
class BookDisplay extends Component{
render(){
return(
<View>
<Text>{this.props.book}</Text>
</View>
)
}
}
<BookDisplay />
를 생성하면서 book이라는 속성을 전달하고 그 값을 문자열 "React Native in Action"으로 지정합니다.
이런 방법으로 props로 전달받은 값은 자식 컴포넌트에서 this.props로 사용할 수 있다.
<동적 속성을 컴포넌트에 전달하는 예>
class MyComponent extends Component{
render(){
let book='React Native in Action'
return(
<BookDisiplay book = {book} />
)
}
}
class BookDisplay extends Component{
render(){
return(
<View>
<Text>{this.props.book}</Text>
</View>
)
}
}
<state를 사용해 동적 속성을 컴포넌트에 전달하는 예>
class MyComponent extends Component{
constructor(){
super()
this.state = {
book: 'React Native in Action'
}
}
render(){
return(
<BookDisiplay book = {this.state.book} />
)
}
}
class BookDisplay extends Component{
render(){
return(
<View>
<Text>{this.props.book}</Text>
</View>
)
}
}
BookDisplay에 props로 전달된 state의 값이 state를 변경하면 어떻게 갱신되는지 살펴보자
props는 변경 불가능하다는 사실을 기억하자, 따라서 부모 컴포넌트인 MyComponent의 state를 변경해야하는데, 이는 BookDisplay의 book props에 새로운 값을 제공하게 되어 부모 컴포넌트와 자식 컴포넌트 모드를 다시 렌더링하게 된다.
state 변수를 선언
state 변수를 갱신하는 setState를 작성
메서드와 state를 props로 자식 컴포넌트에 전달
메서드를 자식 컴포넌트에 있는 터치 핸들러에 연결
<동적 props 갱신하기>
class MyComponent extends Component{
constructor(){
super()
this.state = {
book: 'React Native in Action'
}
}
updateBook(){
this.setState({
book: 'Express in Action'
})
}
render(){
return(
<BookDisiplay
updateBook = {() = this.updateBook()}
book = {this.state.book} />
)
}
}
class BookDisplay extends Component{
render(){
return(
<View>
<Text onPress={this.props.updateBook}>{this.props.book}</Text>
</View>
)
}
}
this.state와 this.props로 state와 props를 참조하면 반복적이 되면서 DRY 원칙 (Don't Repeat Yourself! 반복하지 마라!)을 위배하게 된다.
구조분해할당이란 자바스크립트의 새로운 특징으로 ES2015 스펙에 추가되었으며 리액트 네이티브 앱에서도 사용할 수 있습니다.
구조분해할당 개념은 간단히 말해서 객체에서 속성들을 가져와서 앱에서 변수로 사용하라는 것이다.
const person = {name: 'Jeff', age:22}
const {age} = person
console.log(age) #22
<props와 state 비구조화하기>
class MyComponent extends Component{
constructor(){
super()
this.state = {
book: 'React Native in Action'
}
}
updateBook(){
this.setState({
book: 'Express in Action'
})
}
render
const {book} = this.state
return(
<BookDisiplay
updateBook = {() = this.updateBook()}
book = {book} />
)
}
}
class BookDisplay extends Component{
render(){
const {book, updateBook} = this.props
return(
<View>
<Text onPress={updateBook}>{book}</Text>
</View>
)
}
}
book을 참조할 때 컴포넌트에서 더 이상 this.state와 this.props를 참조하지 않아도 됩니다.
대신에 state와 props에서 book변수를 가져왔으며, 변수 그 자체를 참조할 수 있습니다.
이렇게 함으로써 이해하기 쉬워지고, state와 props가 더 많아지고 복잡해도 코드가 간결해집니다.
stateless 컴포넌트는 속성(props)에 대해서만 신경쓰고 자신의 state가 없기 때문에, 이런 컴포넌트는 재사용해야 하는 컴포넌트를 만들 때 상당히 유용합니다.
stateless 컴포넌트를 사용해 속성에 접근하려면 메서드의 첫번째 인수로 props를 전달합니다.
<stateless 컴포넌트에서의 속성(props)>
const BoookDisplay = (props) => {
const{book, updateBook} = props
return (
<View>
<Text onPress={updateBook}>
{book}
</Text>
</View>
)
}
class MyComponent extends Compnent{
constructor(){
super()
this.state = {
leapYear: true,
info:{
type: 'programming'
}
}
}
render(){
return (
<BookDisplay
leapYear={this.state.leapYear}
info={this.state.info}
topic={['React','React Native', 'JavaScript'}/>
)
}
}
const BookDisplay = (props) => {
let leapYear
let{topics} = props
const {info} = props
topics = topics.map((topic, i) => {
return <Text>This is a leapyear!</Text>
}
if(props.leapYear){
leapyear = <Text>This is a leapyear!</Text>
}
return (
<View>
{leapyear}
<Text>Book type: {info.type}</Text>
{topics}
</View>
)
}
리액트와 리액트 네이티브 컴포넌트를 만들 때, 몇 가지 스첵과 생명주기를 연결해 컴포넌트가 수행하는 동작을 제어할 수 있다.
컴포넌트 스펙(specification) 은 기본적으로 컴포넌트의 생명주기 동안 일어나는 여러 상황에 대해 컴포넌트가 대응하는 방식을 제공한다.
render 메서드는 컴포넌트가 생성될 때 필수적으로 필요한 유일한 메서드이다.
render(){
return(
<View>
<Text>Hello!</Text>
</View>
)
}
render(){
return <View> <Text>Hello!</Text> </View>
}
render(){
return <Somecomponent />
}
render(){
if(something === true){
return <SomeComponent />
}else return <SomeOtherComponent />
}
상태(state)는 생성자에서 만들 수도 있고 속성 초기화(property initializer) 를 사용해서 만들 수도 있다.
props 초기화는 리액트 클래스에서 state를 선언하는 간결한 방법을 제공한다.
class MyComponent extends React.Component{
state={
someNumber: 1,
someBoolean: false
}
}
클래스를 사용하면서 생성자를 사용해 초기 state를 지정할 수 있다.
클래스의 개념은 생성자와 마찬가지로 리액트나 리액트 네이티브에 한정된 것이 아닌 ES2015의 스펙일 뿐이고, 자바스크립트에서 클래스를 이용한 객체 생성과 초기화를 위한 기존의 프로토타입 기반의 상속과 관련해서 문법적으로 추가된 것이다.
컴포넌트 클래스의 생성자에서 다른 속성들도 this.property (property는 지정하고자 하는 속성의 이름)과 같은 문법으로 설정할 수 있습니다.
this 키워드는 사용하고 있는 클래스의 인스턴스를 참조합니다.
constructor(){
super()
this.state = {
someOtherNumber: 19,
someOtherBoolean: true
}
this.name = 'Hello World'
this.type = 'class'
this.loaded = false
}
리액트 클래스는 다른 클래스를 확장해서 만들어지기 때문에 생성자를 이용할 때에는 반드시 super 키워드를 this 키워드 전에 사용해야만 한다.
속성을 이용해서 상태를 지정하는 것은 의도적으로 컴포넌트의 내부에서 사용하는 기능들의 초기 데이터(seed data)를 지정하는 게 아니라면 일반적으로 좋은 방식은 아니다.
state는 컴포넌트가 처음 마운트되거나 생성될 때만 생성된다.
만일 동일한 컴포넌트를 서로 다른 props의 값을 이용해서 다시 렌더링하게 되면 이미 마운팅된 컴포넌트의 인스턴스는 새로운 props의 값으로 상태를 갱신할 수 없다.
<생성자 내에서 속성(props)를 사용해 상태(state)의 값을 지정하고 있다.>
constructor(props){
super(props)
this.state = {
fullName: props.first + ' ' + props.last,
}
}
이들 메서드는 컴포넌트의 생성과 소멸 동안 다른 시점에서 개발자가 지정한 동작을 수행하기 때문에 작동 방식을 이해하는 것은 중요합니다.
리액트 컴포넌트의 생명주기
개발자는 생명주기와 관련해 세 가지 세트의 생명주기 메서드에 연결할 수 있습니다.
갱신은 다음처럼 둘 중 하나로 이루어집니다.
static 클래스 메서드로 컴포넌트가 생성될 때와 컴포넌트가 새 props를 전달받을 때 모두 호출됩니다.
이 메서드는 새로운 props와 가장 최근의 state를 인수로 전달받아서 하나의 객체를 반환한다.
객체의 데이터는 컴포넌트의 상태(state)로 갱신된다.
export default class App extends Component{
state = {
userLoggedIn: false
}
static getDerivedStateFromProps(nextProps, nextState){
if(nextProps.user.authenticated){
return{
userLoggedIn: true
}
}
return null
}
render(){
return(
<View style={styles.container}>
{
this.state.userLoggedIn && (
<AuthenticatedComponent />
)
}
</View>
)
}
}
class MainComponent extends Component{
constructor(){
super()
this.state = {loading: true, data: {}}
}
componentDidMount(){
#simulate ajax call (Ajax 호출을 한다면)
setTimeout(() => {
this.setState({
loading: false,
data: {name: 'Nader Dabit', age: 35}
},2000)
})
}
render(){
if(this.state.loading){
return <Text>Loading</Text>
}
const {name, age} = this.state.data
return(
<View>
<Text>Name: {name}</Text>
<Text>Age: {age}</Text>
</View>
)
}
}
Boolean을 반환하며 개발자로 하여금 컴포넌트의 렌더링을 할 것인지를 결정할 수 있다.
새로운 state, props가 컴포넌트나 자식 컴포넌트의 렌더링이 필요하지 않다고 판단되면 false를 반환한다.
만일 컴포넌트를 다시 렌더링하고 싶다면 true를 반환한다.
class MainComponent extends Component{
shouldComponentUpdate(nextProps, nextState){
if(nextProps.name !== this.props.name){
return true
}
return false
}
render(){
return <SomeComponent />
}
}
class MainComponent extends Component{
componentDidUpdate(prevProps, prevState){
if(prevState.showToggled === this.state.showToggled){
this.setState({
showToggled: !showToggled
})
}
}
render(){
return <SomeComponent />
}
}
class MainComponent extends Component{
handleClick(){
this._timeout = setTimeout(() => {
this.openWidget();
},2000)
}
componentWillUnmount(){
clearTimeout(this._timeout);
}
render(){
return <SomeComponent
handleClick ={ () => this.handleClick()} />
}
}