[React Docs 번역] State and Lifecycle

김승엽
Berkbach
15 min readApr 15, 2020

--

Photo by Fiona Smallwood on Unsplash

State and Lifecycle

이 페이지에서는 React 컴포넌트의 State와 Lifecycle 의 개념을 소개합니다. 이전 글에서 작성했던 시계 예제를 봅시다. ReactDOM.render() 를 호출해서 UI를 업데이트하는 방법을 배웠습니다.

function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);

}

setInterval(tick, 1000);

이번 시간에는 재사용가능하고 캡슐화된 Clock 컴포넌트를 만들어보겠습니다. Clock 컴포넌트는 타이머를 설정하고, 매 초 스스로 업데이트 할 것입니다. 다음 처럼 캡슐화를 진행할 수 있습니다.

function Clock (props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}

setInterval(tick, 1000);

하지만 타이머를 설정하고 매 초 UI를 업데이트 하는 것이 Clock 의 구현 세부사항이 되어야 한다는 사실을 놓치고 있습니다. 이상적으로 우리는 코드를 한번만 작성하고 Clock 이 스스로 업데이트하길 원합니다:

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

이것을 실행 하기 위해서 Clock 컴포넌트에 “state” 를 추가 해야 합니다.
State는 Props와 비슷하지만 private 하고 컴포넌트에 의해 조작됩니다.

Converting a Function to a Class

5 단계로 Clock 과 같은 함수형 컴포넌트를 클래스형 컴포넌트로 바꿀 수 있습니다.

  1. 같은 이름으로 된 React.Component 를 확장하는 ES6 class를 작성합니다.
  2. render() 메소드 하나를 추가합니다.
  3. 함수 의 내용을 render() 메소드 안으로 옮깁니다.
  4. render() 안의 propsthis.props 로 바꿉니다.
  5. 남아 있는 함수 선언문을 지워줍니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

Clock 이 함수형에서 클래스형으로 바뀌었습니다.

render() 메소드는 업데이트가 될 때 마다 호출 될 것 입니다. 하지만 Clock 이 같은 DOM 노드에서 렌더링 되기 때문에 하나의 Clock 인스턴스만 사용됩니다. 이것은 우리에게 state, lifecycle 같은 추가 기능을 사용하게 해줍니다.

Adding Local State to a Class

3 단계를 거쳐서 데이터를 props에서 state로 바꾸겠습니다.

  1. render() 메소드 안의 this.props.datethis.state.date 로 변경합니다.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

2. 초기 this.state를 지정하는 class constructor (생성자) 를 추가합니다.

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>
);
}
}

여기서 어떻게 props를 기본 생성자에 전달하는지 확인하세요.

constructor(props) {
super(props);
this.state = {date: new Date()};
}

클래스형 컴포넌트의 props는 항상 기본 생성자에 의해 호출됩니다.

3. Clock element의 date prop을 지워줍니다.

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

타이머는 나중에 추가하도록 하겠습니다. 결과는 이렇습니다.

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>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

이어서 Clock 의 타이머를 설정하고 매 초 업데이트 하도록 하겠습니다.

Adding Lifecycle Methods to a Class

많은 컴포넌트가 있는 애플리케이션에서 컴포넌트가 제거될 때 컴포넌트의 리소스들을 확보하는 것은 매우 중요합니다.

우리는 DOM에 Clock 이 처음 렌더링 될 때 타이머가 설정 되기를 원합니다. 이것을 React에서 mounting 이라고 부릅니다.

또한 Clock 에 의해 생성된 DOM 이 지워 질 때 마다 타이머가 해제 되기를 원합니다. 이것을 React에서 unmounting 이라고 부릅니다.

컴포넌트가 mount , unmount 될 때 마다 실행되는 특별한 코드를 컴포넌트 클래스에서 선언할 수 있습니다.

class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() { }
componentWillUnmount() { }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

이것을 “LifeCycle 메소드” 라고 부릅니다.

componentDidMount() 메소드는 컴포넌트의 출력값이 DOM에 렌더링이 된 후에 실행합니다. 이 부분이 타이머를 설정하기 좋은 곳입니다.

componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

this (this.timerID)에서 timer ID를 저장하는 방법을 주의하세요.

this.props 는 React에 의해 스스로 설정하고 this.state 가 특별한 의미를 갖지만, 타이머 ID와 같이 데이터 흐름 안에 포함되지 않는 어떤 것을 보관할 필요가 있다면 자유롭게 클래스에 수동으로 부가적인 필드를 추가해도 됩니다.

componentWillUnmount 메소드에서 타이머를 지워보겠습니다.

componentWillUnmount() {
clearInterval(this.timerID);
}

마지막으로 Clock 컴포넌트의 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);
}

tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

이제 타이머가 매 초 업데이트 됩니다. 메소드들이 호출 되는 순서를 빠르게 확인하겠습니다.

  1. <Clock />ReactDOM.render() 로 전달됐을 때, React가 Clock 컴포넌트의 생성자를 호출합니다. Clock 이 현재 시간을 보여줘야하기 때문에 현재 시간을 포함한 객체를 초기화합니다. 우리는 이 state를 나중에 업데이트 합니다.
  2. 다음 React가 Clock 컴포넌트의 render() 메소드를 호출합니다. 이것을 통해 React가 무엇을 화면에 띄어야 할지 알게 됩니다. 다음 React가 Clock 의 렌더링 출력물에 맞춰서 DOM을 업데이트합니다.
  3. Clock 의 출력물이 DOM에 추가될 때, React는 componentDidMount() 메소드를 호출합니다. 그 안에서는, Clock 컴포넌트는 1초의 한번씩 tick() 메소드를 호출하기 위한 타이머를 설정하도록 브라우저에게 요청합니다.
  4. 매 초마다 브라우저가 tick() 메소드를 호출합니다. 그 안에서는 Clock 컴포넌트는 setState() 에 현재 시간을 포함하는 객체를 호출해서 UI 업데이트를 진행합니다. setState() 호출 덕분에 React는 state의 변화를 알게 됩니다. 그리고 화면에 표시할 내용을 알기 위해서 render() 메소드를 다시 호출합니다. 이번에는 render() 메소드의 this.state.date 는 다르기 때문에 렌더링의 출력물은 업데이트된 시간을 포함할 것입니다. React는 그에 맞춰 DOM을 업데이트 합니다.
  5. 만약 Clock 컴포넌트가 DOM에서 한번이라도 삭제된다면, React는 타이머를 멈추기 위해서 componentWillUnmount() 를 호출합니다.

Using State Correctly

setState() 에 대해 알아야할 3가지가 있습니다.

Do Not Modify State Directly

이것은 re-rendering이 일어나지 않습니다.

// Wrong
this.state.comment = 'Hello';

대신, setState() 를 사용해야 합니다.

// Correct
this.setState({comment: 'Hello'});

this.state를 지정할 수 있는 유일한 공간은 바로 생성자 입니다.

State Updates May Be Asynchronous

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있습니다.

this.propsthis.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 됩니다.

이 코드는 counter 업데이트에 실패할 수 있습니다.

// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

고치기 위해서 객체대신 함수형으로된 setState() 의 두번째 양식을 사용합니다. 이 함수는 첫번째 인자로 이전의 state를 받습니다. 두번째 인자로 업데이트된 props를 받습니다.

// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

위에서는 arrow function을 사용했지만, 정규 함수도 작동합니다.

// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});

State Updates are Merged

setState() 가 호출되면 React는 당신이 객체를 현재 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
});
});
}

병합은 얕게 이루어지기 때문에 this.setState({comments})this.state.posts에 영향을 주진 않지만 this.state.comments는 완전히 대체됩니다.

The Data Flows Down

부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트의 state유무를 알 수 없고, 그들이 함수나 클래스로 정의 되었는지에 대해서 관심을 가질 필요가 없습니다. 이것이 state가 대게 로컬 또는 캡슐화로 불리는 이유입니다. state를 보유하거나 설정하지 않은 다른 컴포넌트가 접근할 수 없습니다. 컴포넌트가 본인의 state를 자식 컴포넌트에게 props 로 전달 할 수도 있습니다.

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

이것은 사용자 정의 컴포넌트에서도 사용할 수 있습니다.

<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 date를 props로 받을 것이고 이것이 Clock의 state에서왔는지, Clock의 props에서 왔는지, 하드코딩 됐는지 모릅니다.

function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

이것은 일반적으로 하향식 ( “top-down” ) 또는 단방향 ( “unidirectional” ) 데이터 흐름으로 불립니다. 모든 state는 항상 어떤 특정 컴포넌트가 갖습니다. 그리고 그 state에서 파생된 모든 데이터 또는 UI는 트리 구조에서 “아래” 컴포넌트에게만 영향을 미칠 수 있습니다.

트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 소스 (source) 라고 할 수 있습니다.

독립된 모든 컴포넌트를 보기 위해선, 우리는 3개의 Clock을 렌더링 하는 App 컴포넌트를 만들 수 있습니다.

function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);

Clock 컴포넌트는 스스로 타이머를 설정하고 독립적으로 업데이트합니다.

React 앱에서 컴포넌트가 상태가 있는지 없는지는 시간에 따른 컴포넌트의 실행 세부 사항 결정됩니다. 무상태 컴포넌트 안에서 유상태 컴포넌트를 사용할 수 있고, 그 반대의 경우도 가능합니다.

📚 정리 및 참고 자료

  1. Lifecycle
  2. State와 Props는 비동기적으로 작동합니다.
  3. State를 변경할 때는 setState() 를 사용해야 합니다.

--

--