글 작성자: 개발자 올라프

개요

 

  • 리액트를 다루기에 앞서 리액트 공식문서를 읽어보고 정리하며 공식문서를 읽는 습관, 리액트의 기본적인 개념과 문법을 익히는 시간을 가지고자 한다.
  • 💁‍♂️ 로 표시한 내용은 글을 읽고 난 후 저의 생각정리이며, 나머지는 공식문서의 내용과 일치합니다.

 


 

State and Lifecycle

 

💁‍♂️ React 공식문서 주요개념 5번을 읽는데 state라는 개념이 나왔다. 공식문서 내용을 일일이 생각하고 고민하며 옮겼음에도 불구하고 state가 무엇인지 감이 잡히지를 않았다. 공식문서에는 시계 예시가 나오는데 왜 state를 쓰는지 이해하지 못했다. 왜냐하면 바닐라 자바스크립트에서는 new Date()를 생성하고 시, 분, 초를 뽑아내서 해당 위치에 넣어주면 전자시계가 완성됐기 때문이다. 단순히 변수를 선언하고 값을 집어넣으면 된다는 생각에 끝까지 읽는 내내 왜 저런 코드를 쳐야 하는지 이해할 수 없었던 것 같다. 그래서 유튜브에 'React state'를 검색해서 조금 더 알아보기로 했고 어떤 용도인지 듣고 나서야 이해 가기 시작했다. 리액트 입문하는 입장에서는 공식문서를 보고 바로 이해하기는 힘드므로 state를 왜 쓰는지 먼저 이해하고 읽는 것이 좋다.

 

💁‍♂️ state가 무엇인가? state는 컴포넌트가 가지고 있는 속성 값이다. 속성 값이 변하면 React는 자동으로 UI를 업데이트 시킨다. 그러므로 개발자는 state관리만 잘한다면 화면을 다시 돌리는 작업은 신경 쓰지 않아도 되기 때문에 굉장히 편리하다. (출처 : 코딩앙마 React JS #7 state, useState - 초보자를 위한 리액트 강좌)

 

💁‍♂️ 변수가 있는데 왜 state를 써야 하는가? 아래 상황에서 let post = '우동 맛집'으로 변수 값이 변경되면 <h4>{ post }</h4> 에 있는 { post }에 자동으로 반영되지 않는다. 하지만 useState를 사용한 state는 갑자기 변경되면 state를 사용하던 html 전체가 자동 재렌더링 된다. 즉, 변동 시 자동으로 html에 반영되게 만들고 싶다면 state를 사용한다.

(출처 : 코딩애플 2022 new 리액트 3강 : state 쓰면 뭐가 좋냐면)

function App() {
	let post = '맛집';
    let [글제목, 글제목변경] = useState('추천'); // ustState()의 결과는 [?, ?]의 형태이다. Destructuring 문법사용
    
    return (
    	<div className="App">
        	<div className="list">
            	<h4>{ post }</h4>
            </div>
        </div>
	);
}

 

💁‍♂️ 왜 변수를 써도 재렌더링이 안될까? 우리가 원하는 건 어떤 조건을 만족하면 이벤트가 발생해서 return 값이 바뀐 결과를 확인하고 싶은 것이다. 하지만 리액트는 함수 내에 로직을 작성하는데 올바른 조건 코드를 작성해도 함수는 재실행되지 않으므로 재렌더링 되지 않는 것이다(콘솔을 찍어보면 변수 값은 제대로 바뀌었음을 확인할 수 있다).

(출처 : 생활코딩 React 2022년 개정판 - 7. state)

 

다시 본론으로 State and Lifecycle

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

엘리먼트 렌더링에서 UI를 업데이트하는 한 가지 방법만 배웠다. 렌더링 된 출력 값을 변경하기 위해 ReactDOM.render()를 호출했다. 이 섹션에서는 Clock 컴포넌트를 완전히 재사용하고 캡슐화하는 방법을 배울 것이다. 이 컴포넌트는 스스로 타이머를 설정할 것이고 매초 스스로 업데이트할 것이다.

 

💁‍♂️ toLocaleTimeString(locales, options) 메서드는 날짜의 시간 부분을 언어에 따라 다르게 표현한 문자열을 반환한다.

 

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

시계가 생긴 것에 따라 캡슐화하는 것으로 시작할 수 있다. 그러나 여기에는 중요한 요건이 누락되어 있다. Clock이 타이머를 설정하고 매초 UI를 업데이트하는 것이 Clock의 구현 세부사항이 되어야 한다.

 

💁‍♂️ state를 사용하지 않았지만 props를 사용했기에 재렌더링 되면서 시계 모양이 나왔음을 확인할 수 있다.

위 코드로 작성한 결과 (5초 동안 실행한 결과를 캡처)

 

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

이상적으로 한 번만 코드를 작성하고 Clock이 스스로 업데이트하도록 만들려고 한다. 이것을 구현하기 위해서 Clock 컴포넌트에 "state"를 추가해야 한다. State는 props와 유사하지만, 비공개이며 컴포넌트에 의해 완전히 제어된다.

 

💁‍♂️ prop과 state의 차이점은 props는 컴포넌트를 사용하는 외부자를 위한 데이터이고, state는 컴포넌트를 만드는 내부자를 위한 데이터라는 차이점이 있다. 즉, 위 코드처럼 <Clock />으로 한 번만 가져오고 Clock 컴포넌트 내부 자체적으로 스스로 업데이트를 하기 위해서는 props가 아닌 state를 사용해야 한다는 의미이다.

 

함수에서 클래스로 변환하기

class Clock extends React.Component {
	render() {
    	return (
        	<div>
            	<h1>Hello, world!</h1>
                <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

1. React.Component를 확장하는 동일한 이름의 class를 생성한다.

2. render()라고 불리는 빈 메서드를 추가한다.

3. 함수의 내용을 render() 메서드 안으로 옮긴다.

4. render() 내용 안에 있는 props를 this.props로 변경한다.

5. 남아있는 빈 함수 선언을 삭제한다.

 

Clock은 이제 함수가 아닌 클래스로 정의된다. render 메서드는 업데이트가 발생할 때마다 호출되지만, 같은 DOM 노드로 <Clock />을 렌더링 하는 경우 Clock 클래스의 단일 인스턴스만 사용된다. 이것은 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 해 준다.

 

💁‍♂️ 아직 React 16.8 버전에 추가된 Hook이라는 개념을 익힌 적이 없는데 어떤 특징을 가졌는지 잠깐 살펴보니 'Hook은 클래스 컴포넌트를 작성하지 않아도 state와 같은 특징들을 사용할 수 있다'라고 적혀 있었다. 그래서 클래스 컴포넌트로 변환하는 과정이 있는 것 같다.

공식문서 바로가기 >https://ko.reactjs.org/docs/hooks-state.html

 

클래스에 로컬 State 추가하기

 

세 단계에 걸쳐서 date를 props에서 state로 이동해 본다.

 

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

 

2. 초기 this.state를 지정하는 class constructor를 추가한다.

class Clock extends React.Component {
	constructor(props) { // 클래스 컴포넌트는 항상 props로 기본 constructor를 호출해야 한다.
    	super(props);
        this.state = {date: new Date()};
    }
    
    render() {
    	return (
        	<div>
            	<h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

 

3. <Clock /> 요소에서 date prop을 삭제한다.

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이 스스로 업데이트하도록 만든다"라는 컴포넌트 스스로 업데이트하는 <Clock />을 완성했다. 추가로 Hook을 이용해서 state를 사용할 수 있는 방법에 대해서 공부할 필요가 있는 것 같다.

 

생명주기 메서드를 클래스에 추가하기

 

많은 컴포넌트가 있는 애플리케이션에서 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 것이 중요하다. Clock이 처음 DOM에 렌더링 될 때마다 타이머를 설정하려고 한다. 이것은 React에서 "마운팅"이라고 한다. 또한 Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제하려고 한다. 이것은 React에서 "언마운팅"이라고 한다.

 

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

컴포넌트 클래스에서 특별한 메서드를 선언하여 컴포넌트가 마운트 되거나 언마운트 될 때 일부 코드를 작동할 수 있다. 이러한 메서드들은 "생명주기 메서드"라고 불린다.

 

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

componentDidMount() 메서드는 컴포넌트 출력물이 DOM에 렌더링 된 후에 실행된다. 이 장소가 타이머를 설정하기에 좋은 장소이다.

 

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

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

componentWillUnmount() 생명주기 메서드 안에 있는 타이머를 분해해 본다.

 

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 컴포넌트의 constructor를 호출한다. Clock이 현재 시각을 표시해야 하기 때문에 현재 시각이 포함된 객체로 this.state를 초기화한다. 나중에 이 state를 업데이트할 것이다.

 

2. React는 Clock 컴포넌트의 render() 메서드를 호출한다. 이를 통해 React는 화면에 표시되어야 할 내용을 알게 된다. 그다음 React는 Clock의 렌더링 출력 값을 일치시키기 위해 DOM을 업데이트한다.

 

3. Clock 출력 값이 DOM에 삽입되면, React는 componentDidMount() 생명주기 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 매초 컴포넌트의 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.

 

4. 매초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setState()에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다. setState() 호출 덕분에 React는 state가 변경된 것을 인지하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력 값은 업데이트된 시각을 포함한다. React는 이에 따라 DOM을 업데이트한다.

 

5. Clock 컴포넌트가 DOM으로부터 단 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출한다.

 

State 업데이트는 비동기적일 수도 있다.

 

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다. this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안된다.

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

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

객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용한다. 그 함수는 이전 state를 첫 번째 인자로 받아들일 것이고, 업데이트가 적용된 시점의 props를 두 번째 인자로 받아들일 것이다.

 

State 업데이트는 병합된다.

 

setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합한다. 예를 들어, state는 다양한 독립적인 변수를 포함할 수 있다.

constructor(props) {
	super(props);
    this.state = {
    	posts: [],
        comments: []
    };
}

 

별도의 setState() 호출로 이러한 변수를 독립적으로 업데이트할 수 있다. 병합은 얕게 이루어지기 때문에 this.setState({comments})는 this.state.posts에 영향을 주진 않지만 this.state.comments는 완전히 대체된다.

 

데이터는 아래로 흐른다.

 

부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트가 유상태인지 또는 무상태인지 알 수 없고, 그들이 함수나 클래스로 정의되었는지에 대해서 관심을 가질 필요가 없다. 이 때문에 state는 종종 로컬 또는 캡슐화라고 불린다. state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없다. 컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다.

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

 

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못한다.

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

 

일반적으로 이를 하향식 또는 단방향식 데이터 흐름이라고 한다. 모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 아래에 있는 컴포넌트에만 영향을 미친다.

 

모든 컴포넌트가 완전히 독립적이라는 것을 보여주기 위해 App 렌더링 하는 세 개의 <Clock>을 만든다. 각 Clock은 자신만의 타이머를 설정하고 독립적으로 업데이트한다.

function App() {
	return (
    	<div>
        	<Clock />
            <Clock />
            <Clock />
        </div>
    );
}

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

 

💁‍♂️ 리액트 공식문서 주요 개념 5를 보면서 state가 무엇인지 이해할 수 있었다. 하루동안 공식문서만 보고 state가 무엇인지 이해하기 어려웠는데 state가 어떤 기능인지 검색을 통해 알게 되면서 공식문서 또한 빠르게 이해할 수 있었다. 계속해서 남은 주요개념 6~12를 정리하면서 리액트를 익혀보는 시간을 가질 것이다.

 

'⭐️ Library & Framework > ReactJS' 카테고리의 다른 글

React useEffect 사용하기  (0) 2022.05.19
React Router 사용하기  (0) 2022.05.12
React에 Font Awesome 적용하기  (0) 2022.05.09
React 공식문서 주요개념 읽기 - 1  (0) 2022.05.08
React란 무엇인가?  (0) 2022.05.07