째깍이는 시계 예제를 이전 섹션 중 한 섹션에서 살펴보았습니다. Rendering Elements에서는 UI를 업데이트하는 한가지 방법만을 배워보았습니다. 우린 렌더링된 output을 바꾸기 위해서 root.render()를 호출해야했습니다.
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
}
setInterval(tick, 1000);
이 섹션에서는 어떻게 Clock
컴포넌트를 정말로 재사용가능하게 하고 캡슐화하는지에 대해 배우게 될 것입니다.
이 컴포넌트는 스스로 타이머를 설정할 것이고 매초 스스로 업데이트할 것입니다.
시계가 어떻게 보이는지 캡슐화하는 것으로 시작할 수 있습니다. :
const root = ReactDOM.createRoot(document.getElementById('root'));
//Clock이 어떻게 보이는지를 캡슐화함.
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
root.render(<Clock date={new Date()} />);
}
setInterval(tick, 1000);
그러나, 여기엔 중요한 요건이 누락되어있는데, 그것은 바로 Clock
이 타이머를 세팅하고 매초마다 UI를 업데이트하는 것이 Clock
의 세부사항에 구현되어있어야 한다는 것입니다.
이상적으로는 한번만 코드를 작성하여 Clock
이 스스로 업데이트되길 바랍니다.
이것을 구현하기 위해서, 우린 Clock
컴포넌트에 "state"를 추가해야 합니다.
State는 props과 유사하지만, private하며 완전히 컴포넌트에 의해서만 컨트롤됩니다.
다섯개의 스텝으로 Clock
과 같은 function컴포넌트를 class로 바꿀 수 있습니다.:
React.Component
를 상속합니다.render()
라고 불리는 빈 메서드를 추가합니다. render()
메서드 안으로 옮깁니다.render()
body에 있는 props
을 this.props
으로 바꿉니다.function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
Clock
은 이제 function이 아닌 class로 정의되어졌습니다.
render
메서드는 update가 있을 때 매번 호출되어지겠지만, 같은 DOM node로 <Clock/>
컴포넌트를 렌더링 하는 경우, 한개의 단일 Clock
인스턴스만 사용됩니다.
이러한 점이 local state와 lifecycle메서드와 같은 추가적 기능을 사용할 수 있게 해줍니다.
-> function컴포넌트는 local state와 lifecycle메서드를 사용할 수 없지만, class컴포넌트는 사용이 가능하다고 한다. 그 이유는 class컴포넌트는 인스턴스 하나를 만들어서 사용되는 것이지만, function컴포넌트는 실행될 때 마다 새로 만들어 지는 것이라 state나 lifecycle메서드를 가질 수 없는 것이다.
( ps. 근데 React 16.8버전부터는 function컴포넌트에서도 state와 lifecycle메서드를 사용할 수 있도록 React Hook이라는 것이 도입되었다고 한다. )
세 단계로 props에 있는 date
를 "state"로 이동해보자 :
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
1.render()
메서드 안에 있는 this.props.date
를 this.state.date
로 수정합니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> //this.props -> this.state
</div>
);
}
}
this.state
를 정하는 class constructor를 추가합니다.props
를 기본 constructor에 전달하는지 유의해주세요. class컴포넌트는 항상 props
와 함께 기본 constructor를 호출해야합니다.class Clock extends React.Component {
//this.state를 초기화하는 constructor추가
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
<Clock/>
엘리먼트에서 date
prop을 제거합니다. root.render(<Clock />);
타이머 코드는 나중에 컴포넌트로 만들어서 추가하겠습니다.
결과는 다음과 같습니다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
다음으로 Clock이 스스로 타이머를 설정하고 매초 스스로 업데이트하도록 만들어 보겠습니다.
많은 컴포넌트를 가진 어플리케이션에서는, 컴포넌트가 삭제될 때 해당 컴포넌트가 사용중이던 리소스를 확보하는 것이 중요합니다.
우리는 Clock
이 처음으로 DOM에 렌더링 될때마다 timer를 설정하고 싶습니다. React에선 이것을 "mounting"이라 합니다.
또한 Clock
이 만들어낸 DOM이 삭제될 때 마다 timer를 제거하고 싶습니다. 이를 React에선 "unmounting"이라고 합니다.
우리는 컴포넌트가 mounts되고 unmounts될 때 class컴포넌트에 특별한 메서드를 선언하여 어떤한 코드를 실행할 수 있습니다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
//컴포넌트가 "mount"될 때 (DOM에 처음 추가될 때)
componentDidMount() {
}
//컴포넌트가 "unmount"될 때 (DOM에서 삭제될 때)
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
이러한 메서드를 "lifecycle 메서드"라고 합니다.
componentDidMount()
메서드는 컴포넌트 output이 DOM에 렌더링된 후에 실행됩니다. 이 곳이 timer를 설정하기 좋은 장소입니다.
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
어떻게 this(this.timerID)
에 올바르게 timerID를 저장하는지 주의해주세요.
React에 의해 알아서 this.props
이 설정되고 this.state
는 특별한 의미를 갖게 되지만, 데이터 흐름에 관여하지 않는 timer ID와 같은 어떠한 걸 저장할 필요가 있다면 직접 class에 부가적인 field를 자유롭게 추가해도 괜찮습니다.
componentWillUnmount() lifecycle 메서드 안의 timer를 분해해 보겠습니다.
componentWillUnmount() {
clearInterval(this.timerID);
}
마지막으로, Clock
컴포넌트가 매초 실행시키는 tick()
메서드를 구현해 보겠습니다.
이 tick()메서드는 컴포넌트 로컬 state를 업데이트하기 위해 this.setState()
를 사용해야 합니다.
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);
}
//매초 실행되는 ticke()메서드안에는 this.setState()로
//Clock컴포넌트의 state를 갱신한다.
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
setState()
에 대해 알아야할 3가지가 있습니다.
예를 들어, 아래 코드는 컴포넌트를 다시 렌더링하지 않습니다.
//Wrong
this.state.comment = 'Hello';
대신에, setState()
를 사용하세요~
//Correct
this.setState({comment: 'Hello'});
this.state
를 사용하는 곳은 constructor에서만 가능합니다.
React는 성능을 위해 여러 setState()
호출을 단일 업데이트로 한번에 처리할 수도 있습니다.
왜냐하면 this.props
과 this.state
는 비동기적으로 업데이트 될 수 있어서 다음 state를 계산할 때 해당 value에 의존해서는 안됩니다.
예를들어, 아래 코드는 counter를 업데이트하는데 실패할 수 있습니다. :
//Wrong
this.setState( {
counter: this.state.counter + this.props.increment,
});
이를 올바르게 고치기위해, object보다는 function을 인자로 받는 다른 형태의 setState()
를 사용합시다.
이 function은 첫번째 인자로 이전의 state를 받고 업데이트 시점의 props를 두번째 인자로 받게될 것입니다.
//Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
위에서 arrow function을 이용했지만, 일반적인 functions으로도 동작합니다.
//Correct
this.setState(function(state,props) {
return {
counter: state.counter + props.increment
};
});
setState()
를 호출할 때, React는 당신이 제공한 object를 현재 state에 병합합니다.
예를 들어, 당신의 state는 다양한 독립적인 변수들을 갖고있을 수 있습니다.
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
이때 분리된 setState()
를 호출하여 분리하여 이것들을 업데이트 할 수 있습니다. :
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
병합은 shallow하므로, this.setState({comments})
는 this.state.posts
엔 영향을 끼치진 않지만, this.state.comments
는 완전히 수정됩니다.
부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트가 stateful 한지 stateless한지 알 수 없으며, function으로 정의되었는지 class로 정의되었는지에 대해서 알아야 할 필요도 없습니다.
이 때문에 state가 local 하거나 캡슐화되었다고 불립니다. state를 갖고 있고 설정한 것이 아니라면 어떤 다른 컴포넌트도 접근할 수 없습니다.
컴포넌트는 state를 props으로 자식 컴포넌트에 전달할 수 있습니다.
<FormattedDate date={this.state.date} />
이 FormattedDate
컴포넌트는 props으로 date
를 전달받으며 이것이 Clock
의 state에서 온건지, Clock
의 props에서 온건지 직접 작성된건진 알 수가 없습니다.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
일반적으로 이를 "하향식" 혹은 "단방향식" data 흐름이라고 합니다. 어떤 state든 항상 특정 컴포넌트가 소유하며 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 "아래"에 있는 컴포넌트에만 영향을 미칩니다.
class 컴포넌트가 재사용가능할 수 있는 이유는 "state"를 갖기 때문이라고 한다.
state는 props과 비슷한 면도 있지만 private하며 해당 컴포넌트에 의해서만 관리된다는 차이점이 있다.
또한 class컴포넌트는 React에서 제공하는 lifecycle메서드를 활용하여 상황에 맞는 처리를 lifecycle메서드 안에서 전개해 나갈 수 있다.
예를 들어, 최초로 컴포넌트가 DOM에 등록된 직후에 네트워크 통신을 통해 서버로부터 받아와야 데이터가 있다면 componentDidMount() 메서드를 활용하면 된다.
state에 따라서 변경해야하는 UI가 있으면, 컴포넌트가 갖고있는 state를 업데이트하여 render()함수를 호출하고 업데이트된 내용이 사용자의 스크린에 보여지게 되는 것이다.
하지만 state를 업데이트할 때 주의해야 할 점은 state오브젝트를 직접 수정해서는 안된다는 것이다!
state자체를 직접 수정할 경우엔 컴포넌트를 다시 렌더링 하지 않아서 변경된 것이 스크린에 반영되지 않기 때문이다.
따라서, state를 업데이트할 땐 setState()로 업데이트해주자.
다음으로 주의할 점은 setState()는 setTimeout함수와같이 비동기 함수이므로 setState()가ㅏ 호출된 바로 다음 render()가 바로 호출될 것이란 순서를 보장하지 못한다는 점이다.
따라서, state를 업데이트할 때 이전 state를 기준으로 무언가 게산 되어지는 경우라면,
컴포넌트 내의 state값에 의존해서 업데이트 하기보다는 (object를 업데이트하기보다는)
function을 활용해 state자체를 인자로 받은 것을 토대로 아예 새로운 state만들어 반환하여 업데이트하는 것이 좋다.
object를 인자로 받아 state를 업데이트하는 방식
//Wrong
this.setState( {
counter: this.state.counter + 1,
})
⭐function을 인자로 받아 state를 업데이트하는 방식⭐
//Correct (방법1)
this.setState((state, props) => ({
counter: state.counter + 1
}));
//Correct (방법2)
this.setState(function(state,props) {
return {
counter: state.counter + 1
};
});