안녕하세요.
Hynn 입니다. 이번 포스팅에서는 생명주기(Life Cycle), 그리고 State 에 대해서 알아보도록 하겠습니다.
React 의 기초사항으로 이 포스팅은 아마도 총 12개의 주제로 모두 세분화하여 업데이트를 할 예정입니다.
그럼 시작해보도록 하겠습니다.
==========
==========
1. State 및 생명주기(Life-Cycle) 개념 알아보기
먼저 이전 포스팅에서 사용한 시계를 React 에서 구현한 코드를 가지고 그대로 사용을 해보도록 하겠습니다.
이전 포스팅, Element 단위 이해하기 포스팅에서 다루었던 코드를 그대로 활용할 예정입니다.
이전 포스팅에서 UI 를 업데이트 하는 방법은, 오직 "root.render()" 를 이용하여 출력하는 것을 배웠고, 이때 초당 새롭게 그려내기 위해 "setInterval" 함수를 추가로 구현하여, 매 초마다 시계를 새로고침하는 형태로 그린 바 있습니다.
아래의 코드 예시 중 React 부분만을 살펴보도록 하겠습니다.
const Tick = () => {
return (
<div>
<h1>Hynn Tistory Blog</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Tick />)
setInterval(() => root.render(<Tick />), 1000)
위 함수를 살펴보면, 먼저 'Tick' 이라는 변수(Component) 를 선언하고, Return 값에 JSX 문법을 이용하여, Element Render 를 위한 값을 넣었습니다.
그리고 실제 시계가 될 값에는, " { } " 을 사용하여, 값을 가져오도록 설정되어 있습니다.
그리고 React 의 기본 사용법인 "root" 라는 변수에 "createRoot" 를 담고, "root.render" 를 사용하여 Component Tick 을 Render 하도록 하고, setInterval 을 사용하여, 매 초마다 Tick Component 를 랜더링 하도록 작성이 되어 있습니다.
하지만 코드상으로는 결과값이 같더라도, 기존의 코드는 전체를 새롭게 그리는 코드임에는 변하지 않습니다.
이를 우리는 이상적으로 코드를 작성하면, 시간이 스스로 업데이트 하도록 할 수 있어야 합니다.
이를 위해 사용하는 것이, 바로 "State" 입니다. 또한 이러한 Cycle 이 Life-Cycle, 즉 생명주기라고 합니다.
2. 기존의 Component (Function ) 을 Class 로 바꿔 사용하기.
변경하는 방법은 아주 간단합니다.
이전 포스팅에서 우리는 이미 Component 를 다루어보았고, 이를 기존의 JavaScript 에서 생성자함수라고 이야기하던 "Class"로 변환하는 작업이 필요합니다.
이를 간단하게 코드로 구현하면 아래와 같이 구현이 가능합니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hynn Tistory Blog</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Clock />)
위의 함수는 이제 생성자 함수로서 변경이 되었습니다.
즉 React 의 관점에서는 "Class Component" 로 다시 작성된 것입니다.
하지만 위 작업만으로는 아까와 같은 결과물이 출력되지 않습니다. "setInterval" 과 같은 매 초마다 랜더링하는 구조가 들어있지 않기 때문에, 페이지가 로드되는 시점의 시간만 나타날 겁니다.
이를 위해 몇가지 method 에 대한 이해와, 생명주기에 대한 이해가 필요하게 되었습니다.
여기서 필요한 method 는 3가지가 존재합니다.
이를 실제 사용할 코드와 같이 예제를 살펴보도록 하겠습니다.
constructor(props){
super(props)
this.state = {
date: new Date()
}
}
componentDidMount(){
this.timerID = setInterval(()=> this.tick(),1000)
}
componentWillUnmount(){
clearInterval(this.timerID)
}
위 3가지의 개념은 반드시 이해가 필요할 겁니다.
1) Constructor
생성자 함수를 사용할때도 보았던 method 입니다. React Class Component 에서는 이 Class Compnment 를 DOM에 마운트 하기 전에 최초에 호출되는 method 입니다. 즉 "Initial", 상태를 초기화하고, method 를 component instacne 에 binding 할 때 사용됩니다.
먼저 Constructor method 에는 반드시 "super" 가 따라와야 합니다.
기존의 JavaScript 에서의 생성자함수 Class 는, extends, 즉 상속된 class 일경우에 super 를 사용합니다.
React 에서도 구문 자체는 동일합니다.
즉, 위 class component 는 "React.Component" 라는 Class Component 를 상속받으므로, 반드시 "super" 가 포함됩니다.
또한 "Props" 인자가 반드시 포함되어야 합니다.
이전 포스팅에서 언급했듯이, Props 는 React 에서 가장 중요하게 다루어야 하며, 모든 매개변수, Propertiy 가 모여있는 객체이기도 합니다.
이를 이용해서 어떠한 매개변수, State 모두 Props 에 담기게 됩니다.
즉 Constructor 는 Class Component 를 최초 Initialize 하는 method 로 이해하면 되겠습니다.
즉 Class Component 의 초기값을 결정짓는 method 입니다.
2) componentDidMount, componentWillUnmount
이 외에도 여러가지 다양한 Life-Cycle method 가 존재합니다.
이 method 는 공통적으로 "Component" 가 DOM에 마운트 된 후 호출됩니다.
Component 가 DOM 에 Mount, 즉 이것은 React 의 Component 가 렌더링되어 웹 페이지의 일부가 되었음을 의미합니다.
즉 위의 예시로 본다면, "Clock" 이라는 Class Component 가 Constructor method 로 Initialize 된 뒤에, render 가 진행되고, 그 뒤에 호출되는 method 입니다.
다시말해, 이 Clock 라는 class Component 는 이미 웹 페이지, Browser 에 랜더링 되어, 이미 웹 페이지의 구성요소로 표시되고 난 이후에 호출되는 method 입니다.
이 Life-Cycle Method 는 3가지의 단계로 이루어져 있습니다.
1. Mounting
2. Updating
3. Un-Mounting
이전 포스팅에서 우리가 한가지 기억해야 할 중요한 점은 "State", 즉 상태는 "수정"할 수 없다는 것을 이해해야 합니다.
State 는 최초 Initialize 가 되고 난 이후에는 "수정"을 할 수 없습니다. 바로 "업데이트" 하는 것에 대한 의미가 이해되어야 합니다.
따라서 이 생명주기라고도 하는 Life-Cycle 은 이 3개의 순서도를 반드시 따라야 React 에서 구성요소를 효율적으로 동작하고, 업데이트할 수 있습니다.
3) Life-Cycle
대표적으로 Mount, Update, Unmount 에서 사용되는 대표적인 method 와 간단한 의미는 아래와 같습니다.
- Mount : Mount Method 는 컴포넌트가 처음 생성되고 DOM에 삽입하는 생명주기의 첫번째 단계입니다.
- Update : 업데이트 단계는 컴포넌트의 Props 나 state 가 변경되었을 때 발생합니다. 생명주기의 두번째 단계입니다.
- Unmount :이 단계는 컴포넌트가 DOM 에서 제거되기 전에 호출됩니다.
Method | 설명 | |
Mount | Contructor | 이 method 는 첫번째로 호출되며, 컴포넌트의 초기 상태를 설정할 때 사용되는 method 입니다. |
static getDerivedStateProps | contructor 이후에 호출되는 정적(static) 메소드입니다. 이전 "props" 를 기반으로, 컴포넌트의 초기 상태를 설정하는데 사용됩니다. |
|
render | 이 Method 는 컴포넌트를 DOM 에 랜더링하는데 사용됩니다. 모든 React 컴포넌트에는 반드시 포함되어야 하는 Method 입니다. |
|
componentDidMount | 이 Method 는 컴포넌트가 DOM에 랜더링되고 난 이후에 호출됩니다. API 에서 데이터를 가져오거나, DOM에 접근(Access) 하는 설정 작업을 수행할 때 필요합니다. |
|
Update | static getDerivedStateFromProps | 이 Method 는 컴포넌트의 "Props"가 변경될 때 마다 다시 호출됩니다. 즉 변경되는 Props를 기반으로 컴포넌트의 상태를 업데이트할 때 사용됩니다. |
shouldComponentUpdate | 이 Method 는 컴포넌트가 업데이트 되어야하는지에 대한 여부를 결정할 때 사용됩니다. 즉 React 에서 랜더링이 되기 전 불필요한 랜더링을 피해 성능을 최적화 할 때 사용됩니다. |
|
render | 업데이트 측면의 Render Method 는 이러한 업데이트 된 컴포넌트를 다시 DOM 에 랜더링 할 때 사용됩니다. |
|
getSnapshotBeforeUpdate | 이 Method 는 컴포넌트의 변경 내용이 DOM에 반영되기 전에 호출됩니다. 즉 컴포넌트가 업데이트 되기 전 스크롤 위치같은 DOM의 정보를 캡처하는 데 사용됩니다. |
|
componentDidUpdate | 이 Method 는 컴포넌트의 변경사항이 DOM에 반영된 "이후" 에 호출됩니다. 이 Method 에서는 주로 3rd-Party Library 를 업데이트하는 등, 업데이트된 DOM에 의존하는 사이드 이펙트를 수행할 때 사용됩니다. |
|
Unmount | componentWillUnmount | 이 Method 는 컴포넌트가 DOM에서 제거되기 전 호출됩니다. 즉 DOM 에서 이벤트리스너를 제거하는 등, 정리작업을 수행할 때 사용됩니다. |
이제 이 생명주기를 이용한 실제 코드의 전체를 살펴보고, 어떠한 흐름으로 작동이 이루어지는지 살펴보도록 하겠습니다.
class Clock extends React.Component {
constructor(props){
super(props)
this.state = {
date: new Date()
}
}
componentDidMount(){
this.timerID = setInterval(()=> this.tick(),1000)
}
tick(){
this.setState({
date: new Date()
})
}
render() {
return (
<div>
<h1>Hynn Tistory Blog</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Clock />)
먼저 Clock 이라는 Class Component 는 호출되는 시점에 먼저 Constructor method 가 호출되면서, 초기설정이 이루어집니다.
이 constructor 에는 상태값에 현재 시간의 "date" 속성이 포함됩니다.
그리고 난 뒤 "componentDidMount" Method 가 동작하면서, "this.timerID" 에 setInterval 에 의해 반환된 TimerID 값을 저장하는 변수입니다.
그리고 나서 Tick method 에서는 이전에 "componentDidMount" 에서 설정된 타이머에 의해 매 초마다 호출되는 메소드입니다.
즉, Tick 이라는 Method 내의 setState method 를 이용해서 컴포넌트의 date 속성을 현재시간으로 "업데이트" 합니다. 그로 인해서 이 컴포넌트는 매 초마다 다시 랜더링되고 , 새로운 시간이 표시됩니다.
이제 이를 바탕으로 페이지가 실제로 랜더링이 이루어지는 "render" method 가 호출됩니다.
그로 인해 모든 작업은 완료가 됩니다.
이를 바탕으로 본다면, 실행순서는 아래와 같습니다.
- Constructor
- componentDidMount
- Tick
- Render
Tick 은 엄연히 따진다면 생명주기에서 "Update" 에 해당하는 Method 이지만, 바로 Mount 에 해당하는 componentDidMount 의 "timerID" 에서 이를 호출하고 있습니다. 따라서, Tick 이 먼저 호출되며, Render 의 경우, 생명주기에서 Mount 에도 해당되지만, Updating 에도 포함되기 때문에, 순서상 가장 마지막에 호출되는 Method 가 되었습니다.
물론 내부적으로는 아래와 같이 순서를 이해하셔도 틀리진 않습니다.
- Constructor
- render
- componentDidMount
- Tick
- Render
즉, 최초에 constructor 이후에 랜더를 한차례 한 이후에, DOM에 마운트가 된 상태로써, 3,4번이 매 초마다 상태를 업데이트 하며, 랜더링이 이루어지고 있는 구조이기 때문입니다.
3. State 사용하기
이제 위에서 언급한 State 를 자세하게 살펴보도록 하겠습니다.
이 State 사용에 가장 중요한 점은 3가지가 존재합니다.
- State 는 "수정"하는게 아니라, "업데이트" 하는 것입니다.
- State 업데이트는 동기/비동기적일 수 있습니다.
- State 업데이트는 병합(Merge) 됩니다.
이를 하나씩 살펴보도록 하겠습니다.
1) State는 "수정"이 아니라 "업데이트"
"State"를 지정하는 것은 오로지 생명주기에서 Mount 에서 생성해야 합니다.
특히나, Mount 에서도 최초 Initialize 에 사용되는 "Constructor" 에서만 사용되어야 합니다. 그리고 "setState" method 를 사용해야 합니다.
아래와 같이 작성하는 것이 올바른 예시입니다.
class Clock extends React.Component {
constructor(props){
super(props)
this.state = {
date: new Date()
}
기존의 JavaScript 구문을 이해한다면 어렵지 않습니다. 아래의 순서를 지켜서 작성한다면 말입니다.
- Class Component 는 생성자 함수인 Class 를 사용하고, React.Component 의 자식클래스입니다.
- 초기 Initialize 를 위해 constructor method 를 사용하고, 매개변수로 "Props" 를 사용합니다.
- 자식 Class 이므로 Super, 매개변수로 동일하게 "props" 를 사용합니다.
- 이제 "state" 를 지정해야 합니다. 이 state 는 항상 "객체" 형태로 처리되며, key:values 형태로 지정해야 합니다.
- key 는 date, value 는 new Date() 로 현재 시간을 저장합니다.
- 여기서 this 는 Clock을 의미합니다. 따라서 이 State 는 Clock 이라는 Class 의 state 로 저장됩니다.
2) State 업데이트는 동기/비동기적
React 에서의 State 는 일반적으로 "동기" 업데이트로 이루어집니다.
하지만 성능을 위해 여러 "setState" 호출을 단일 업데이트로 한꺼번에 처리하기도 합니다. 이를 비동기적 업데이트라고도 합니다.
이를 이용해 예시를 나뉘어 보면 조금더 쉽게 이해할 수 있습니다.
먼저 이를 이해하기 위해, 두가지의 타입을 이해보도록 하겠습니다.
이를 위해, render 에 두개의 console.log 를 생성하여, 어떠한 타입인지를 살펴보도록 하겠습니다.
class Clock extends React.Component {
constructor(props){
super(props)
this.state = {
date: new Date()
}
}
componentDidMount(){
this.timerID = setInterval(()=> this.tick(),1000)
}
// componentWillUnmount(){
// clearInterval(this.timerID)
// }
tick(){
this.setState({
date: new Date()
})
}
render() {
console.log(this.props)
console.log(this.state)
return (
<div>
<h1>Hynn Tistory Blog</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Clock />)
이를 이용해, 설정해보면, Props 는 "{} ", state 는 시간이 표시되고 있습니다.
즉 State 와, Props 는 엄연히 다릅니다.
여기서의 this.props 와 this.state 는 각각 다른 방식으로 사용되고, 목적이 다릅니다.
이 두가지의 뜻을 간단하게 알아볼 필요가 있습니다.
- this.props : 부모 Component 에서 자식 Component 로 전달되는 속성을 저장하는 객체, 읽기 전용으로 설정되며, 데이터를 표시할 때만 사용할 수 있고, 변경할 수 없습니다.
- this.state : 컴포넌트의 내부 상태를 저장하는 객체, 사용자 입력 or API 호출결과와 같이 시간이 지남에 따라 변경될 수 있는 데이터를 추적할 수 있고, "setState" 를 사용하여 값을 "업데이트" 할 수 있음.
즉, props 의 값은 가져올 수 만있고, state 는 "업데이트"할 수 있다는 차이가 존재합니다.
3)State는 병합됨
setState 를 호출하면, React 는 제공된 객체를 현재 State 로 "병합"합니다.
먼저 예시로 State 를 몇가지 설정해보도록 하겠습니다.
constructor(props) {
super(props);
this.state = {
posts: '',
comments: '',
};
}
위의 예시를 본다면, posts, comments 라는 state 가 존재하고 값은 null 값으로 부여되어 있습니다.
이를 병합한다는 의미는 아래의 코드를 이해하면 쉽게 이해가 가능합니다.
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
위의 코드를 예시로 본다면, 첫번째의 this.setState 가 업데이트 되면, State 안에는 아래와 같이 설정이 됩니다.
this.state = {
posts: response.posts,
comments: response.comments
};
}
즉, 각각의 개별적인 setState 로 호출했더라도, state 안에는 이 value 들이 병합되어, 하나의 객체 안에 담기게 됩니다.
이는 예시에 있는 얇은 병합이지만, 일반적으로 setState 를 사용하여 업데이트 하게 된다면, 이 State 객체안에 모두 담기게 됩니다.
4.데이터 흐름도 이해하기
React 는 JavaScript 와 다른점이 존재합니다.
기존의 JavaScript 에서는 Hoisting 이라고 하는 개념, 혹은 작성하는 코드의 순서에 따라 실행되는 순서나 흐름도가 바뀔 수 있습니다.
하지만 React 에서는 "Top-Down" 이라고 하는 단방향 흐름으로 이루어 집니다.
예시를 작성해보면 아래와 같습니다.
const Square = (props) => {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
)
}
class Board extends React.Component {
renderSquare(i){
return <Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
}
render(){
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
)
}
}
class Game extends React.Component {
constructor(props){
super(props)
this.state = {
history : [{
squares:Array(9).fill(null),
}],
stepNumber : 0,
xIsNext : true,
}
}
handleClick(i){
const history = this.state.history.slice(0, this.state.stepNumber +1)
const current = history[history.length -1]
const squares = current.squares.slice()
if(caculateWinner(squares) || squares[i]){
return
}
squares[i] = this.state.xIsNext ? 'X':'O'
this.setState({
history : history.concat([{
squares : squares,
}]),
stepNumber : history.length,
xIsNext : !this.state.xIsNext,
})
}
jumpTo(step){
this.setState({
stepNumber : step,
xIsNext : (step % 2) === 0,
})
}
render(){
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = caculateWinner(current.squares)
const moves = history.map((step, move)=>{
const desc = move ? 'Go to move # ' + move : 'Go to game start'
return (
<li key={move}>
<button onClick={()=> this.jumpTo(move)}>{desc}</button>
</li>
)
})
let status
if(winner){
status = 'Winner :' + winner
} else{
status = 'Next Player : ' + (this.state.xIsNext ? 'X' : 'O')
}
return (
<div className="game">
<div className="game-board">
<Board
squares = {current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
)
}
}
const root =ReactDOM.createRoot(document.getElementById('root'))
root.render(<Game />)
이후에 다루게 될 코드이지만, 이 코드는 아래와 같은 형태로 작성이 되어 있습니다.
즉, Game 이라는 가장 최상위의 Component 에서 Board 로, Board 에서 각각의 Square 로, 흐름이 이루어져 있습니다.
즉, Game 의 State 는 자식 컴포넌트인 Board 에게 Props 로 전달합니다.
그리고 Board 의 State 는 자식 컴포넌트인 Square 에게 Props 로 전달합니다.
위의 설명을 대입하자면 아래와 같이 이해해야 합니다.
Game 에 적용된 State 는 Game 내에서는 업데이트가 가능합니다.
하지만 Board 는 Game 의 state 를 props 로 전달받기 때문에, "읽기 전용" 으로 되기 때문에, 이를 업데이트 할 수 없습니다.
하지만 실제로는 업데이트를 할 수 있는데 아래와 같은 구조로 업데이트 해야 합니다. 이는 단방향 데이터 흐름을 유지하고 React 의 컴포넌트를 사용하는데 아주 중요한 원칙이라고 할 수 있습니다.
만약 Game 의 State 를 Board 에서 업데이트 해야 하는 경우를 가정하면 아래와 같이 적용해야 합니다.
- Board 에서 Callback 를 사용합니다.
- 전달된 Props 를 바탕으로 Game 컴포넌트에서 State 를 업데이트 해야 합니다
위 데이터 흐름을 React 가 갖는 가장 큰 특징 중 하나인 " 단방향 데이터 흐름" , Top-Down 방식으로 칭합니다.
이 내용을 정리하면 아래와 같은 개념을 이해해야 합니다.
- React 에서 모든 Component 는 State 를 가질 수 있습니다.
- State 는 자식 Component 에게 Props 로 전달할 수 있다
- 자식 Component는 부모 Component State 를 직접 업데이트 할 수 없다
- 자식 Component가 부모 Component 상태를 업데이트 해야하는 경우, 부모 Component 는 자식 Component 에게 Callback Function 을 전달하여, 업데이트를 처리해야 합니다. 이렇게 하면 자식 Component 는 업데이트 된 데이터와 함께 이 함수를 호출하고, 부모 Component 는 이 데이터를 기반으로 상태를 업데이트 합니다.
State 와 생명주기에 대해서 알아보았습니다.
다음 포스팅에서는 이제 React 에서 이벤트를 어떻게 처리하는지에 대한 기본 구성을 알아보도록 하겠습니다.
감사합니다.
'개발공부일지 > React' 카테고리의 다른 글
React - 조건부 랜더링 (0) | 2023.02.28 |
---|---|
React - 이벤트 처리하기 (0) | 2023.02.27 |
React - Component & Props 이해하기 (0) | 2023.02.24 |
React - Element 단위 이해하기 (1) | 2023.02.23 |
React - JSX 문법 기초 이해하기 (0) | 2023.02.23 |
댓글