<리액트를 다루는 기술> 3장 컴포넌트

|

velopert님의 <리액트를 다루는="" 기술=""> 개정판을 보고 공부한 것을 정리합니다.

  • 리액트에서는 ‘컴포넌트’라는 구성단위로 프론트 개발을 하게 된다.
    • 컴포넌트: 구성요소
    • 화면을 구성하는 요소들을 각각 만들고 그것을 합쳐 하나의 페이지가 되는 형식
  • 이 컴포넌트 또한 작게 쪼개서 작은 부분을 합쳐 하나의 컴포넌트로 만들 수 있다. (하나의 큰 컴포넌트는 다른 작은 컴포넌트들의 조합이다, 라는 점이 함수형 프로그래밍을 생각나게 하는듯….)
  • 하나의 컴포넌트 안에서 쪼개고 쪼개기를 반복하면 하나의 작은 tree 구조처럼 될 수 있다. 큰 요소 안에 작은 요소가 있으니 부모 자식의 관계가 형성될 수 있다.

클래스형 컴포넌트

리액트를 사용하여 애플리케이션의 인터페이스를 설계할 때 사용자가 볼 수 있는 요소는 여러 가지 컴포넌트로 구성되어 있다.

뒤에서 만들어 볼 일정 관리 애플리케이션에 사용된 컴포넌트

  1. TodoTemplate: 현재 화면의 중앙에 있는 사각형 레이아웃 컴포넌트
  2. TodoInput: 새로운 항목을 추가할 수 있는 컴포넌트
  3. TodoList: 할 일 항목을 여러 개 보여주는 컴포넌트
  4. TodoItem: 각 항목을 보여 주기 위해 사용되는 컴포넌트

컴포넌트의 기능은 단순한 템플릿 이상이다. 데이터가 주어졌을 때 이에 맞추어 UI를 만들어 주는 것은 물론, 라이프사이클 API를 이용하여 컴포넌트가 화면에서 나타날 때, 사라질 때, 변화가 일어날 때 주어진 작업들을 처리할 수 있으며, 임의 메서드를 만들어 특별한 기능을 붙여 줄 수 있다.

앞에서 보았던 App 컴포넌트는 함수형 컴포넌트이며, 코드가 다음과 같은 구조로 이루어져 있다.

import React from 'react';
import './App.css';

function App() {
  connst name = '리액트';
  return <div className="react">{name}</div>;
}

export default App;

컴포넌트를 선언하는 방식은 두 가지가 있다: 함수형 컴포넌트, 클래스형 컴포넌트

클래스형 컴포넌트는 다음과 같이 선언할 수 있다.

import React, {Component} from 'react';

class App extends Component {
  render() {
    const name = 'react';
    return <div className="react">{name}</div>;
  }
}

export default App;

클래스형 컴포넌트와 함수형 컴포넌트의 차이점은 클래스형 컴포넌트의 경우 이후 배울 state 기능 및 라이프사이클 기능을 사용할 수 있다는 것과 임의 메서드를 정의할 수 있다는 것이다.

ES6의 클래스 문법 ES6 이전에는 자바스크립트에 클래스(class)가 없었다. 개념 자체는 있었지만, 그것을 구현하려면 class 대신 prototype을 사용하여 작성해야 했다.

function Dog(name) {
  this.name = name;
}

Dog.prototype.say = function() {
  console.log(this.name + ':멍멍');
}
var dog = new Dog('검둥이');
dog.say(); // 검둥이: 멍멍

ES6 문법부터는 이와 기능이 똑같은 코드를 class를 사용하여 다음과 같이 작성할 수 있다.

class Dog {
  constructor(nname) {
    this.name = name;
  }
  say() {
    console.log(this.name + ':멍멍');
  }
}

const dog = new Dog('흰둥이');
dog.say(); // 흰둥이: 멍멍

클래스형 컴포넌트에서는 render 함수가 꼭 있어야 하고, 그 안에서 보여 주어야 할 JSX를 반환해야 한다.

함수형 컴포넌트의 장점

  1. 클래스형 컴포넌트보다 선언하기가 훨씬 편하다.
  2. 메모리 자원도 클래스형 컴포넌트보다 덜 사용한다.
  3. 프로젝트를 완성하여 빌드한 후 배포할 때도 함수형 컴포넌트를 사용하는 것이 결과물의 파일 크기가 더 작다. (아주 적은 차이)

함수형 컴포넌트의 단점 state와 라이프사이클 API의 사용이 불가능하다 -> 리액트 v16.8 업데이트 이후 Hooks라는 기능이 도입되면서 해결됨 완전히 클래스형 컴포넌트와 똑같이 사용할 수 있는 것은 아니지만 조금 다른 방식으로 비슷한 작업을 할 수 있다.

리액트 공식 매뉴얼에서는 컴포넌트를 새로 작성할 때 함수형 컴포넌트와 Hooks를 사용하도록 권장.

첫 컴포넌트 생성

  1. 파일 만들기
  2. 코드 작성하기
  3. 코듈 내보내기 및 불러오기

src 디렉터리에 MyComponent.js 파일 생성

import React from 'react';

const MyComponent = () => {
    return <div>나의 새롭고 멋진 컴포넌트</div>;
};

export default MyComponent;

ES6의 화살표 함수

  • arrow function
  • ES6 문법에서 함수를 표현하는 새로운 방식
  • 주로 함수를 파라미터로 전달할 때 유용.
  • 기존 function을 대체할 수 없다. 서로 가리키고 있는 this 값이 다르다!
function BlackDog() {
  this.nname = '흰둥이';
  return {
    name: '검둥이',
    bark: function() {
      console.log(this.name+':멍멍!');
    }
  }
}

const blackDog = new BlackDog();
blackDog.bark(); // 검둥이:멍멍!

function WhiteDog() {
  this.name = '흰둥이';
  return {
    name: '검둥이',
    bark: () => {
      console.log(this.name+':멍멍!');
    }
  }
}

const whiteDog = new WhiteDog();
whiteDog.bark(); // 흰둥이: 멍멍!

일반 함수는 자신이 종속된 객체를 this로 가리키며, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.

const triple = (value) => value * 3;

따로 {}를 열어 주지 않으면 연산한 값을 그대로 반환한다는 의미

함수형 컴포넌트를 선언할 때에는 큰 차이가 없다.

모듈 내부내기 및 불러오기

export default MyComponent 코드는 다른 파일에서 이 파일을 import할 때, 위에서 선언한 MyComponent 클래스를 불러오도록 설정한다.

import React from 'react';
import MyComponent from './MyComponent';

const App = () => {
  return <MyComponent />;
};

export default App;

import 구문은 위와 같이 사용한다.

props

  • props는 properties를 줄인 표현으로 컴포넌트 속성을 설정할 때 사용하는 요소
  • props 값은 해당 컴포넌트를 불러와 사용하는 부모 컴포넌트(이 상황에서는 App 컴포넌트를 부모 컴포넌트로 상정)에서 설정할 수 있다.

JSX 내부에서 props 렌더링

  • props 값은 컴포넌트 함수의 파라미터로 받아 와서 사용할 수 있다.
import React from 'react';

const MyComponent = props => {
  return <div>안녕하세요,  이름은 {props.name}입니다.</div>;
};

export default MyComponent;

컴포넌트를 사용할 때 props 값 지정하기

import React from 'react';
import MyComponent from './MyComponent';

const App = () => {
  return <MyComponent name="React" />;
}

위의 코드에서 name="React" 하는 부분을 자식 컴포넌트가 props.name으로 받아와서 보여줄 수 있는 것이다.

리액트에서 props는 Immutable data이다.

  • props는 상위 컴포넌트에서 하위 컴포넌트로 값을 전달할 때 사용한다.
  • 리액트의 Data Flow는 단방향 형식으로 부모에서부터 자식으로 이동하기 때문에 거꾸로 올라갈 수 없다.
  • 따라서 props에 있는 데이터들은 수정이 불가능하며, 오직 안에 있는 값을 꺼내서 사용할 수 있다.

props 기본값 설정: defaultProps

import React from 'react';

const MyComponent = props => {
  return <div>안녕하세요,  이름은 {props.name}입니다. </div>;
};

MyComponent.defaultProps = {
  name: '기본 이름',
};

export default MyComponent;

태그 사이의 내용을 보여주는 children

  • 리액트 컴포넌트를 사용할 때 컴포넌트 태그 사이의 내용을 보여 주는 props (일종의 예약어?)
import React from 'react';
import MyComponent from './MyComponent';

const App = () => {
  return <MyComponent>리액트</MyComponent>;
};

export default App;
  • MyComponent에서 props.children 값으로 가져올 수 있다.

비구조화 할당 문법을 통해 props 내부 값 추출하기

  • 비구조화 할당 문법? (destructuring assignment)
    • 객체 안에 있는 값을 추출해서 변수 혹은 상수로 바로 선언해줄 수 있도록 하는 ES6 문법 ```javascript const object = { a: 1, b: 2};

const { a, b } = object;

console.log(a); // 1 console.log(b); // 2

function print({ a, b }) { console.log(a); console.log(b); }

print(object);


```javascript
import React from 'react';

const MyComponent = props => {
  const { name, children } = props;
  return (
    <div>
      안녕하세요, 제 이름은 {name}입니다. <br/>
      children 값은 {children}입니다.
    </div>
  );
};

MyComponent.defaultProps = {
  name: '기본 이름',
};

export default MyComponent;
  • 함수의 파라미터 부분에서도 사용 가능
import React from 'react';

const MyComponent = ({ name, childeren }) => {
  return (
    <div>
    안녕하세요,  이름은 {name}입니다. <br/>
    children 값은 {children}입니다.
    </div>
  );
};

MyComponent.defaultProps = {
  name: '기본 이름',
};

export default MyComponent;

propTypes를 통한 props 검증

  • propTypes를 통해 컴포넌트의 필수 props를 지정하거나 props의 타입을 지정할 수 있다.
  • 우선 코드 상단에 import PropTypes from 'prop-types';
MyComponent.propTypes = {
  name: PropTypes.string,
};
  • 잘못된 type의 props를 넘겨줘도 값이 나타나기는 하지만, 콘솔에 경고 메시지가 출력된다.
Warning: Failed prop type: Invalid prop 'name' of type 'number' supplied to 'MyComponetn', expected 'string'.
  in MyComponent (at App.js:5)
  in App (at src/index.js:7)

isRequired를 사용하여 필수 propTypes 설정

MyComponent.propTypes = {
  favoriteNumber: PropTypes.number.isRequired,
};
  • 마찬가지로 값이 나타나기는 하지만, 콘솔에 경고 메시지가 출력된다.
Warning: Failed prop type: The prop 'favoriteNumber' is marked as required in 'MyComponent', but its value is 'undefined'.
  in MyComponent (at App.js:5)
  in App (at src/index.js:7)

더 많은 PropTypes 종류

  • array: 배열
  • arrayOf(다른 PropType): 특정 PropType으로 이루어진 배열
    • 예) arrayOf(PropTypes.number) -> 숫자로 이루어진 배열
  • bool: true 혹은 false 값
  • func: 함수
  • number: 숫자
  • object: 객체
  • string: 문자열
  • symbol: ES6의 Symbol
  • node: 렌더링할수 있는 모든 것(숫자, 문자열, 혹은 JSX 코드. children도 node PropType이다.)
  • instanceOf(클래스): 특정 클래스의 인스턴스
    • 예) instanceOf(MyClasss)
  • oneOf([‘dog’, ‘cat’]): 주어진 배열 요소 중 값 하나 (도메인 같은 느낌인듯)
  • oneOfType([React.PropTypes.string, PropTypes.number]): 주어진 배열 안의 종류 중 하나
  • objectOf(React.PropTypes.number): 객체의 모든 키 값이 인자로 주어진 PropType인 객체
  • shape({ name: PropTypes.string, num: PropTypes.number }): 주어진 스키마를 가진 객체
  • any: 아무 종류

클래스형 컴포넌트에서 props 사용하기

class MyComponent extends Component {
  render() {
    const { name, favoriteNumber, children } = this.props; // 비구조화 할당
    return (
      <div>
        안녕하세요,  이름은 {name}입니다. <br/>
        children 값은 {children}입니다. <br/>
        제가 좋아하는 숫자는 {favoriteNumber}입니다.
      </div>
    );
  }
}

MyComponent.defaultProps = {
  name: '기본 이름',
};

MyComponent.propTypes = {
  name: PropTypes.string,
  favoriteNumber: PropTypes.number.isRequired,
};

export default MyComponent;
  • defaultProps와 propTypes를 설정할 때 class 내부에서 지정하는 방법도 있다.
class MyComponent extends Component {
  static defaultProps = {
    name: '기본 이름'
  };
  static propTypes = {
    name: PropTypes.string,
    favoriteNumber: PropTypes.number.isRequired,
  };
  render() {
    const { name, favoriteNumber, children } = this.props;
    return (...);
  }
}

export default MyComponent;

state

  • state는 사용자(클라이언트)와의 더욱 dynamic한 통신을 위해 만들어졌다.
  • state는 컴포넌트의 특정 상태를 기억하여 화면에 반영하고, 상태가 사용자에 의해 변경되며 다시 화면이 변경되는 기능을 하기 위해 존재하는 객체.
  • props와 다르게 컴포넌트 내부에서 바뀔 수 있다.
  • 하위에서 상위로 event를 통해 값을 전달한다.
  1. 클래스형 컴포넌트의 state
  2. 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state

클래스형 컴포넌트의 state

import React, { Component } from 'react';

class Counter extends Component {
    constructor(props) {
      super(props);
      // state의 초깃값 설정하기
      this.state = {
        number: 0,
      };
    }
    render(){
        const { number } = this.state; // state를 조회할 때는 this.state로 조회
        return (
            <div>
                <h1>{number}</h1>
                <button
                    onClick={() => {
                        // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
                        this.setState(
                            { number: number + 1 });
                    }}
                >
                    +1
                </button>
            </div>
        );
    }
}

export default Counter;
  • 컴포넌트에 state를 설정할 때는 constructor 메서드를 작성하여 설정한다.
  • constructor란 컴포넌트의 생성자 메서드
  • 클래스형 컴포넌트에서 constructor를 작성할 때는 반드시 super(props)를 호출해 주어야 한다.
    • super(props)를 호출하지 않으면 this.props 사용 시 생성자 내에서 정의되지 않아 버그 발생 가능성이 생긴다.
  • 이 함수가 호출되면 현재 클래스형 컴포넌트가 상속받고 있는 리액트의 Component 클래스가 지닌 생성자 함수를 호출해 준다.
  • 그 후 this.state 값에 초깃값을 설정한다. 컴포넌트의 state는 객체 형식이어야 한다.
  • 이벤트로 설정할 함수를 넣어 줄 때는 화살표 함수 문법을 사용하여 넣어 주어야 한다.
    • 화살표 함수의 this 바인딩 특성
  • 함수 내부에서는 this.setState라는 함수를 사용하여 state 값을 바꿀 수 있게 해 준다.

state를 constructor에서 꺼내기

import React, { Component } from 'react';

class Counter extends Component {
        state = {
            number: 0,
            fixedNumber: 0
        };
    render(){
        const { number, fixedNumber } = this.state;
        return (...);
    }
}

export default Counter;
  • 위 방식대로 하면 constructor 메서드를 선언하지 않아도 state의 초깃값을 설정할 수 있다.
    • 자동적으로 constructor 메서드 호출?

this.setState에 객체 대신 함수 인자 전달하기

  • this.setState를 사용하여 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트된다.
  • 그렇다면, 만약 onClick에 설정한 함수 내부에서 this.setState두 번 호출한다면 어떻게 될까?
onClick={() => {
  this.setState({number: number + 1});
  this.setState({number: this.state.number + 1});
}}
  • 상상해볼 수 있는 것: number가 한 번에 2씩 더해진다.
  • 하지만?
  • 실제로는 1씩 더해진다. (여전히)
  • 왜?
  • this.setState는 비동기적으로 상태를 업데이트한다. -> this.setState를 사용한다고 해서 state값이 바로 바뀌지 않는다.
  • 해결책? -> this.setState를 사용할 때 객체 대신 함수를 인자로 넣어준다.
this.setState((prevState, props) => {
  return {
    // 업데이트하고 싶은 내용
  }
})
  • prevState: 기존 상태
  • props: 현재 지니고 있는 props. 생략 가능.
<button
  // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정
  onClick={() => {
    this.setState(prevState => {
      return {
        number: prevState.number +1
      };
    });
    // 위 코드와 아래 코드는 완전히 똑같은 기능
    // 아래 코드는 함수에서 바로 객체를 반환한다는 의미
    this.setState(prevState => ({
      number: prevState.number +1
    }));
  }}
  >
    +1
</button>
  • 화살표 함수에서 값을 바로 반환하고 싶다면 코드 블록 {}을 생략하면 된다.
const sum = (a,b) => a + b;

this.setState가 끝난 후 특정 작업 실행하기

  • setState의 두 번째 파라미터로 콜백(callback) 함수 등록
<button
    onClick={() => {
        this.setState(
            {
                number: number + 1
            },
            () => {
                console.log('방금 setState가 호출되었습니다.');
                console.log(this.state);
            }
        )
    }}
>
    +1
</button>

Component constructor 주의 사항

  • setState()를 생성자 안에서 호출하지 않는다.
    • 생성자 내에서는 setState를 사용하는 것이 아닌 this.state로 초기값을 할당해주어야 한다.
    • 생성자는 this.state를 직접 할당할 수 있는 곳으로, 그 외에는 꼭 this.setState()를 이용.
  • 생성자 내에서는 구독 작업이나 외부 API를 호출하면 안된다.
    • 외부 API 호출이 필요하다면 생성자 밖에서 componentDidMount()를 사용
  • stateprops를 복사하면 안 된다.
    • this.state = { color: props.color };
    • 불필요한 작업 (this.props.color를 직접 사용하면 된다.)
    • color props의 값이 변하더라도 state에 반영되지 않기 때문에 버그를 발생시킬 여지가 있다.
    • props의 갱신을 의도적으로 무시해야 할 때만 이와 같은 패턴을 사용하는 것이 좋지만, 이 경우에도 해당 props의 이름을 initialColor 등으로 변경하는 것이 자연스럽다.
    • 그러면 이후 필요에 따라 컴포넌트가 key를 변경하여 초기 state를 재설정하도록 강제할 수 있다.

함수형 컴포넌트에서 useState 사용하기

  • 리액트 16.8 버전 이후부터 가능
  • Hooks를 통해 가능

useState 사용하기

import React, {useState} from 'react';

const Say = () => {
    const [message, setMessage] = useState('');
    const onClickEnter = () => setMessage('안녕하세요!');
    const onClickLeave = () => setMessage('안녕히 가세요!');

    return (
        <div>
            <button onClick={onClickEnter}>입장</button>
            <button onClick={onClickLeave}>퇴장</button>
            <h1>{message}</h1>
        </div>
    )
}

export default Say;
  • useState 함수의 인자에는 상태의 초깃값을 넣어 준다.
    • 클래스형 컴포넌트에서의 state 초깃값과 달리, 반드시 객체가 아니어도 상관없다.
    • 숫자, 문자열, 객체, 배열,…
  • useState에서 반환하는 배열
    • 배열의 첫 번째 원소: 현재 상태 (‘message’)
    • 배열의 두 번째 원소: 상태를 바꾸어 주는 함수 (세터Setter함수) (‘setMessage’)
  • 배열 비구조화 할당을 통해 이름을 자유롭게 정해준 것

한 컴포넌트에서 useState 여러 번 사용하기

import React, {useState} from 'react';

const Say = () => {
    const [message, setMessage] = useState('');
    const onClickEnter = () => setMessage('안녕하세요!');
    const onClickLeave = () => setMessage('안녕히 가세요!');

    const [color, setColor] = useState('black');

    return (
        <div>
            <button onClick={onClickEnter}>입장</button>
            <button onClick={onClickLeave}>퇴장</button>
            <h1 style=>{message}</h1>
            <button style= onClick={() => setColor('red')}>
                빨간색
            </button>
            <button style= onClick={() => setColor('green')}>
                초록색
            </button>
            <button style= onClick={() => setColor('blue')}>
                파란색
            </button>
        </div>
    )
}

export default Say;
  • 여러 개의 상태를 하나의 컴포넌트에서 관리할 수 있다!

state를 사용할 때 주의 사항

  • 클래스형 컴포넌트, 함수형 컴포넌트 모두 해당
  • state는 초기값 설정이 필수이다.
  • state 값을 바꾸어야 할 때는 setState 혹은 useState를 통해 전달받은 세터 함수를 사용해야 한다.

잘못된 코드 예시

// class component
this.state.number = this.state.number +1;
this.state.array = this.array.push(2);
this.state.object.value = 5;

// functional component
const [object, setObject] = useState({ a: 1, b: 1 });
object.b = 2;
  • 왜냐하면 react는 state가 변경될 때마다 변경된 부분을 감지하여 리렌더링을 하는데, setState 메서드를 사용하지 않고 직접 state 값을 수정할 경우 변경을 감지하지 못해서 리렌더링을 하지 못한다.

  • 배열이나 객체를 업데이트해야 할 때는 배열이나 객체 사번을 만들고 그 사본에 값을 업데이트한 후, 그 사본의 상태를 setState 혹은 세터 함수를 통해 업데이트한다!

정리

  • 앞으로 새로운 컴포넌트를 만들 때는 useState를 사용할 것을 권장한다.
  • 이로써 코드가 더 간결해짐
  • 리액트 개발 팀이 함수형 컴포넌트와 Hooks를 사용하는 것이 주요 컴포넌트 개발 방식이 될 것이라고 공지

참고

더 알아보기..

Comments