React 입문(2) : states ~ LifeCycle API(1) (last update: 2020.12.15.)

devpark·2020년 12월 15일
0

React.js & JavaScript

목록 보기
2/11

Introduction


어제 학습한 props에 이어 오늘은 state에 대한 개념과 ES6 문법의 react내 활용,
그리고 LifeCycle API 및 그 활용에 대해 공부해 보았다.


마찬가지로 이 글 역시 notion의 해당 페이지에 백업되었다.
React.js 기초 학습 통합 페이지 바로가기


2-4. state

import React, { Component } from 'react';
class Counter extends Component{
  state = {
    number : 0
  }
  handleIncrease = () => {
    this.setState({
      number: this.state.number + 1
    });
  }
  handleDecrease = () => {
    this.setState({
      number : this.state.number - 1
    });
  }
  render(){
    const wrapperStyle = {
      padding: '20px',
      margin: '20px',
      border: '2px solid magenta',
      backgroundColor: 'teal',
      color: 'white',
      paddingBottom: '40px',
      width: '10rem',
      textAlign: 'center'
    }
    const buttonStyle = {
      margin: '.4rem',
      backgroundColor: 'yellow',
      color: 'navy',
      borderRadius: '3rem'
    }
    const resultStyle = {
      fontSize: '1.2rem',
      fontWeight: 'bold',
      color: 'lime'
    }
    return(
      <React.Fragment>
      <div style={ wrapperStyle }>
        <h1>COUNTER</h1>
    <div style= { resultStyle }>VAL : { this.state.number }</div>
    <button onClick={ this.handleIncrease } style={ buttonStyle }>+</button>
    <button onClick={ this.handleDecrease } style= { buttonStyle }>-</button>
      </div>
      </React.Fragment>
    );
  }
}
export default Counter;

동적인 데이터를 다룰 땐 state를 사용하게 된다. 위의 코드에서 Component의 state를 정의할 때는 class fields 문법을 사용해서 정의했다.

class fields 문법을 사용하지 않는 경우에는 다음과 같이 state를 정의할 수 있다:

import React, { Component } from React;
class Counter extends Component{
  constructor(props){
    super(props);
    this.state = {
      number : 0
    }
  }
  ...
}

위에서 사용한 방법은 Constructor(생성자)를 통한 방법이다. 위 코드의 Constructor에서 super(props)를 호출한 이유는 우리가 Component를 생성할 때 Component를 상속했기 때문에 (class Counter extends Component에 해당하는 부분) Constructor를 작성하게 되면 기존의 class 생성자를 덮어쓰게 된다. 때문에, React Component가 가지고 있던 생성자를 super를 통해 미리 실행하고, 이후 진행할 작업(state 설정)을 진행하는 것이다. 단, class fields와 constructor를 함께 사용하는 경우는 class fields가 먼저 실행되고, 이후 constructor에서 설정한 값이 도출되게 된다.


1. states : Type Method

// Method 1. Arrow Functions
handleIncrease = () => {
	this.setState({
		number: this.state.number + 1
	});
}
handleDecrease = () => {
	this.setState ({
		number: this.state.number - 1
	});
}
// Method 2. Constructors
constructor(props){
	super(props);
	this.handleIncrease = this.handleIncrease.bind(this);
	this.handleDecrease = this.handleDecrease.bind(this);
}

Component에서의 메소드는 1. Arrow Functions(화살표 함수)를 사용하는 방법, 2. Constructors(생성자)를 사용하는 방법 이렇게 두 가지로 작성이 가능하다. 이 두 가지 방법이 아닌 단순 메소드로 작성하는 경우 함수가 버튼의 클릭 이벤트로 전달되는 과정에서 this와의 연결이 끊겨 undefined로 도출되게 된다.


2. setState

각 메소드에 존재하는, state에 있는 값을 바꾸기 위해서는 반드시 this.setState를 무조건 거쳐야 한다. 리액트에서는, 이 함수가 호출되면 Component가 re-randering되도록 설계되어 있으며, 이 setState는 객체로 전달되는 값만 업데이트를 해 준다.

코드의 예시를 들면 다음과 같다:

state = {
	number: 0;
	foo: 'bar'
}

예를 들어 state가 복수의 값을 갖는 경우, 그리고 this.setState({ number: 1 }을 통해 number state의 값을 업데이트하게 된다면, 또 다른 state내 값인 foo의 값, 'bar'는 그대로 남고 number 값만 1로 업데이트 된다.

하지만 setState는 객체 내부까지 확인하지는 못한다.

예를 들어 state가 다음과 같이 설정되어 있는 경우 :

state = {
	number: 0,
	foo: {
		bar: 0,
		foobar: 1
	}
}

아래와 같이 setState를 한다고 해서 foobar의 값이 업데이트 되지는 않는다.

this.setState({
	foo: {
		foobar: 2
	}
})

또한 아래와 같이 작성하게 된다면 기존의 foo 객체 자체가 바뀌게 된다.

{
number: 0,
foo{
		boobar: 2
	}
}

따라서 위와 같은 상황에서는 다음과 같이 코드를 작성하여야 한다:

this.setState({
	number: 0,
	foo: {
		...this.state.foo,
		foobar: 2
	}
});

여기에서의 ...은 ES6의 Rest Operator(전개 연산자)이다. 이는 기존의 객체 안에 있는 내용을 해당 위치에 풀어준다는 의미로, 설정하고 싶은 값을 또 넣어주면 해당 값을 덮어쓰게 된다. 이러한 작업은 이후 immutable.js, immer.js를 사용하여 보다 간단하게 작업할 수 있다.

또한 setState를 사용한 값의 업데이트는 아래와 같은 코드로 전개할 수 있다 :

// Method 1. Destructing Assignment(1)
this.setState(
({ number }) => ({
	number: number + 1
	})
);
// Method 2. Destructuring Assignment(2)
const { number } = this.state;
this.setState({
number: number + 1
});

3. Destructuring Assignment Expression with Rest Operator

위에서 작성한 코드는 기존의 코드에서의 (state)를 ({ number })로 변경한 것으로, Destructuring Assignment 즉 비 구조화 할당이라는 문법이다. ES6에서 새로 추가되었으며, 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript Expression(표현식)이다.

이해하기 쉬운 예제 코드는 아래와 같다:

// Method 1. Basic Syntax (Array)
const catList = ["CHEETAH", "LILLY", "KKOKKO"];
const lastCat = catList[0];
const secondCat = catList[1];
const firstCat = catList[2];
// Method 2. With Destructuring Assignment (Array)
const [lastCat, secondCat, firstCat] = ["CHEETAH", "LILLY", "KKOKKO"];

위의 코드에서 Method1은 Destructuring Assignment Expression을 사용하지 않고 기본 문법대로 작성한 것이며, Method2는 Destructuring Assignment Expression을 사용하여 간단하게 작성한 것이다. Method2에서 좌항은 호출될 변수명의 Array, 우항이 할당할 값을 의미한다. 또한 각 요소에는 같은 index를 갖는 배열 값이 할당되므로 Method1과 같이 별도로 index설정을 해 주지 않아도 된다!


2-5. Rest Operator & Spread Operator

위의 코드를 Rest Operator와 결합해서 사용할 수도 있다.

그 예시는 다음과 같다:

const [lastCat, ...cats] = ["CHEETAH", "LILLY", "KKOKKO"];
console.log(lastCat); // result : CHEETAH
console.log(cats); // result : LILLY KKOKKO 

Rest Operator(전개 연산자) = ... 를 활용하면 좌항에서 명시적으로 할당되지 않은 나머지 배열 값을 사용할 수 있다. 우항의 값인 CHEETAH, LILLY, KKOKKO 중 lastCat이라는 이름으로 CHEETAH를 선언하였고, 선언된 CHEETAH를 제외한 나머지 값들은 ...(Rest Operator)cats라는 이름에 담기게 된다.

그렇다면 같은 모양(...)처럼 보이는 Rest Oeprator/Spread Operator는 어떻게 구분하는가?
아주 잘 설명된 도큐먼트가 있어 우선 이 곳에 공유한다.

ES6: What is the difference between Rest and Spread?

도큐먼트에서 설명한 바에 따르면 이 둘의 차이는 다음과 같다 :

내용을 해석하면 이러하다.

Rest SyntaxSpread Syntax와 완벽하게 같아 보인다. 어떤 면에서는, rest 문법은 spread 문법의 반대이다. Rest Syntax가 여러 개의 구성 요소들을 수집하고 그들을 하나의 구성 요소로 접지 하는 반면에, Spread Syntax는 하나의 배열 안의 그 구성 요소들을 "확장한다."

해당 도큐먼트에서 발췌하여, 그 결론은 다음과 같다.

Summary : Rest Syntax / Spread Syntax

(The spread operator allows us to spread the value of an array (or any iterable) across zero or more arguments in a function or elements in an array (or any iterable). The rest parameter allows us to pass an indefinite number of parameters to a function and access them in an array. -MDN)

Spread Operators는 하나의 배열 또는 iterable 안의 구성 요소(elements) 또는 함수 내의 0 또는 그 이상의 Arguments(인수)를 전역에서 값을 뿌려주도록 허용한다.

Rest Operators는 무한한 수의 Parameters(인자)를 하나의 함수에 넘겨주고 한 배열에서 그들에게 접근할 수 있도록 허용한다.

따라서, 문법의 형태는 ... 으로 같을 지라도, 그 쓰임에 따라서 문법이 달라지게 된다.


2-6. Events

...
render(){
	return(
		<React.Fragment>
			<div>
				<hi>COUNTER</h1>
				<div>
					VAL : { this.state.number }
				</div>
				<button onClick={ this.handleIncrease }>+</button>
				<button onClick={ this.handleDecrease }>-</button>
			</div>
		</React.Fragment>
	);
}

각 요소, 즉 <button>에 onClick 이벤트가 호출되도록 설정해 두었다. js 문법에 익숙하다면 해당 문법이 어렵지 않게 느껴지겠지만, react에서 onClick 이벤트를 대입할 때에는 별도의 규칙이 존재한다:

  1. 이벤트 이름은 camelCasing 한다.
  2. 이벤트에 전달하는 값은 메소드가 아닌 함수여야 한다. (메소드 호출시 랜더링이 무한 반복된다.)

3. LifeCycle API

react Component가 사용될 떄 각 상황에 따라 호출되는 LifeCycle API에 대해 알아본다.

3-1. Component 초기 생성 시

먼저, Component가 브라우저에 나타나기 전/후에 호출되는 API들은 다음과 같다:

1. constructor

constructor(props){
	super(props);
}

Component Constructor 함수이다.
Component가 새로 생성될 때 마다 이 함수가 호출된다.

2. componentWillMount(deprecated)

componentWillMount(){
}

Component가 화면에 띄워지기 직전에 호출되는 API로, 본래 브라우저가 아닌 환경(Server-side)도 호출하는 용도로 사용했지만 react v16.3 기준으로 deprecated되었다. v16.3 이후로는 UNSAFE_componentWillMount()라는 이름으로 사용된다. 이 API에서 하던 작업들은 constructor와 componentDidMount API로도 충분히 처리가 가능하다.

3. componentDidMount

componentDidMount(){
}

Component가 화면에 노출될 때 호출된다.
이 API로는 D3, masonry와 같이 DOM을 사용해야 하는 외부 라이브러리 연동이나, 해당 Component에서 필요로 하는 데이터를 요청하기 위해 axios, fetch 등을 통한 ajax 요청, DOM의 속성을 읽거나 직접 변경하는 작업을 진행하게 된다.


3-2. Component Update시

Component의 업데이트는 props 및 state의 변화에 따라 결정된다.

1. componentWillReceiveProps(deprecated)

componentWillReceiveProps(nextprops){
}

Component가 새로운 props를 받게 되었을 때 호출되는 API이다. 이 API의 내부에서는 주로 state가 props로 변환해야 하는 로직을 작성한다. 새로 받게 될 props는 nextProps로 조회할 수 있으며, 이 때 this.props를 조회하면 업데이트 되기 전의 API, 즉 변경 이전의 props가 조회된다. 이 API 또한 v16.3부터 deprecated되었다. 이후의 버전에서는 UNSAFE_componentWillReceiveProps()라는 이름으로 사용되며, 이 기능은 상황에 따라 새로운 API getDerivedStateFromProps로 대체될 수 있다.

2. (new)static getDerivedStateFromProps()

static getDerivedStateFromProps(nextProps, prevState){
	// e.g.
	if(nextProps.value !== prevState.value){
		return { value: nextProps.value }; 
	}
		return null; // 별도의 업데이트 예정이 없는 경우
}

이 함수는 v16.3 이후에 만들어진 LifeCycle API로, setState가 아닌 특정 props로 받아온 값을 설정하고 싶은 state로 리턴하는, 즉 input으로 받은 props 값을 state로 동기화 하는 작업을 수행해야 할 때 사용된다.

3. shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState){
return true;
}

Component를 최적화하는 작업에서 주로 사용되는 API로, re-rendering 여부를 리턴하는 함수이다. 기본적으로 return true;를 반환하며, 인자로 nextProps와 nextState를 받는다. 이 말인 즉슨, react의 핵심인 re-randering을 위해 기존의 props 및 state와 신규 props와 state를 비교하여 변동 사항이 존재하는 경우 re-rendering 하도록 만든다는 것이다. 여기서 리-렌더링이란 곧 render(){} 함수의 호출을 의미하며, return false;로 설정하는 경우 render(){} 함수를 호출하지 않게 된다.(다시 말해, DOM 조작을 하지 않고 Virtual DOM에만 렌더링하게 된다.)

참고로 react 내부에서 변화가 발생한 부분을 감지하기 위해서는 Virtual DOM에 그리는 작업을 필요로 한다. 또한 현재 컴포넌트의 상태가 업데이트되지 않아도, 부모 컴포넌트가 리-렌더링되면 자식 컴포넌트들도 렌더링되게 되므로, 컴포넌트의 수가 많아지면 이 Virtual DOM의 렌더링으로도 CPU 자원이 어느 정도 소모된다. 이러한 경우 불필요한 CPU 처리량을 줄여주기 위해서, 즉 Virtual DOM에서의 re-rendering또한 방지하고자 할 때 return false; 처리를 할 수 있다.

4. componentWillUpdate(deprecated)

componentWillUpdate(nextProps, nestState){
}

shouldComponentUpdate에서 true를 return했을 때만 호출되며, 만약 false를 return한 경우라면 이 함수는 호출되지 않는다. 주로 애니메이션 효과를 초기화하거나, eventListener를 없애는 작업을 이 곳에 대입하며, 함수가 호출된 후에는 render(){}가 호출되게 된다. 이 API 또한 v16.3 이후 deprecated 되었다. 기존의 기능은 getSnapshotBeforeUpdate로 대체될 수 있다.

5. (new)getSnapshotBeforeUpdate() also e.g. Component

이 API가 발생하는 시점은 다음과 같다:

  1. render(){}
  2. getSnapshotBeforeUpdate()
  3. 실제 DOM에 변화 발생
  4. componentDidUpdate

이 API를 통해서, DOM 변화가 일어나기 직전의 DOM 상태를 가져오고, 여기서 반환하는 값은 componentDidUpdate에서 3번째 인자로 받아올 수 있게 된다. 원리의 이해를 위해, 아래의 예제 코드를 직접 해체해보며 한 줄 한 줄 분리해 해석해 보자.

e.g.

import React, { Component } from 'react';
// Component를 사용하기 위해 'react'에서 'Component'라는 key를 대입하여 import한다.
import './ScrollBox.css';
// /.ScrollBox.css/ 경로에서 css 파일을 import한다.
// Component를 상속한 ScrollBox 클래스를 선언한다.
class ScrollBox extends Component{
	// 변수명 id의 값은 2로 설정한다.
  id = 2;
  state = {
	// 배열형 state의 구성 요소는 array라는 이름이며, 이는 1번 index를 가진다.
    array: [1]
  };
	// 화살표 함수 handleInsert를 선언하고,
  handleInsert = () => {
		// state를 설정하는데,
    this.setState({
			// state의 요소이자 배열인 'array'의 구성요소는 id값의 후위연산자며, 
			// 그 외는 state의 array, 즉 미연산 값이다.
      array: [this.id++, ...this.state.array]
    });
  };
	// 변동사항이 존재하는 경우, 업데이트 전에 스냅샷을 찍는 LifeCycle API인데,
	// 인자로 이전(previous)Props와 이전(previous)State를 받는다.
  getSnapshotBeforeUpdate(prevProps, prevState){
		// 만약 이전 시점 state의 array(state의 요소이자 요소명이 'array')와
		// 현재 시점 state의 array가 같지 않다면 = 즉 변동 사항이 있다면,
    if (prevState.array !== this.state.array){
				// const list로 { scrollTop, scrollHeight }으로 선언한다.
        const { scrollTop, scrollHeight } = this.list;
				// 그리고 이 scrollTop, scrollHeight를 반환한다.
        return {
          scrollTop,
          scrollHeight
        };
    }
  }
	// component 업데이트가 진행되면 해당 함수를 호출하며,
	// 인자로 이전의 props, 이전의 state, snapshop을 받는다.
  componentDidUpdate(prevprops, prevState, snapshot){
		// snapshot이 존재한다면,
    if(snapshot){
			// 위에서 리턴된 scrollTop을 list 기존값으로 대입한다.
      const { scrollTop } = this.list;
			// scrollTop이 snapshot의 scrollTop과 같지 않다면 리턴한다.
      if (scrollTop !==snapshot.scrollTop) return;
			// const diff는 list의 scrollHeight에서 snapshot의,
			// 즉 업데이트 이전의 scrollHeight를 뺀 것이다.
      const diff = this.list.scrollHeight - snapshot.scrollHeight;
			// list의 scrollTop에서 const diff의 값을 더한다.
      this.list.scrollTop += diff; 
    }
  }
	// 리-렌더링을 진행한다.
  render(){
		// const rows는 state의 요소명 array의 map으로 선언하며,
		// 이 map은 Arrow Function으로, className "row"의 div에
		// { number }를 key로 대입하며, { number }를 본문으로 갖는다.
    const rows = this.state.array.map(number => (
      <div className= "row" key={ number }>
        { number }
      </div>
    ));
	// 반환 내용은 다음과 같다 :
	// 현재 (const)list를 ref로 선언하며, className을 "list"로 설정한다.
	// 위에서 담은 1. const rows를 본문으로 갖는 div와
	// 2. handleInsert 함수를 onClick으로 호출하는 button.
  return(
    <div>
      <div
        ref= { ref => {
          this.list = ref;
        }}
        className="list"

        { rows }
        </div>
          <button onClick={ this.handleinsert }>CLICK ME</button>
    </div>
    ); 
  }
}
// 이 위까지 작성한 ScrollBox Component를 기본값으로 export한다.
export default ScrollBox;

profile
아! 응응애에요!

0개의 댓글