[리액트를 다루는 기술] 3장 컴포넌트

devHagaa·2022년 6월 22일

이 포스팅은 김민준님의 '리액트를 다루는 기술'을 요약한 글입니다.

3.1 클래스형 컴포넌트

컴포넌트를 선언하는 방식은 함수형 컴포넌트와 클래스형 컴포넌트가 있습니다.

클래스형 컴포넌트와 함수형 컴포넌트의 역할은 같으나 둘의 차이점은 클래스형 컴포넌트의 경우 state 기능 및 라이프사이클 기능 사용 가능한 것과 임의 메서드 정의할 수 있다는 것입니다.

  • 클래스형 컴포넌트에서는 render 함수가 꼭 있어야 하고, 그 안에서 JSX를 반환 해야 합니다.
  • 함수형 컴포넌트 장점 : 선언 편함. 메모리 자원도 클래스형 보다 덜 사용. 프로젝트 완성 후 빌드 배포시 파일 크기가 더 작음. (성능과 파일 크기는 사실 크게 차이 없음). 단점 : state와 리사이클 API의 사용이 불가능 → Hooks 도입으로 해결
  • 이 책에서는 함수형 컴포넌트랑 Hooks 사용을 추천합니다.

3.2 첫 컴포넌트 생성

3.2.2 코드 작성하기

먼저 함수형 컴포넌트로 작성하고, 나중에 클래스형 컴포넌트로도 작성해 보겠습니다.

//MyComponent.js

import React from 'react';

const MyComponent = () => {
	return <div>새로운 컴포넌트<div>;
};

export default MyComponent;

이 코드에서는 함수를 작성할 때 function 키워드를 사용하는 대신에 () ⇒ {}를 사용하여 함수를 만들어 주었습니다. 이는 ES6에 도입된 화살표 함수 문법입니다.
화살표 함수

3.2.3 모듈 내보내기 및 불러오기

// MyComponent.js

const MyComponent = () => {...};

//export를 통해 모듈을 내보낸다
export default MyComponent;
// App.js

// import를 통해 MyComponent.js에서 MyComponent 컴포넌트를 불러온다
import MyComponent from './MyComponent';

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

export default App;

3.3 props

props는 properties로 컴포넌트 속성 설정시 사용하는 요소입니다.

props 값은 부모 컴포넌트 (ex> 샘플에선 App) 에서 설정할 수 있습니다.

3.3.1 JSX 내부에서 props 렌더링

// MyComponent.js

import React from 'react';

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

export default MyComponent;

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

// App.js

import MyComponent from './MyComponent';

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

export default App;

안녕하세요, 제 이름은 React입니다.

3.3.3 props 기본값 설정 : defaultProps

// MyComponent.js

import React from 'react';

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

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

export default MyComponent;
// App.js

import MyComponent from './MyComponent';

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

export default App;

안녕하세요, 제 이름은 기본 이름입니다.

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

// MyComponent.js

import React from 'react';

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

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

export default MyComponent;
// App.js

import MyComponent from './MyComponent';

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

export default App;

출력값

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

객체에서 값을 추출하는 문법을 비구조화 할당(destructuring assignment)이라고 부릅니다. 이 문법은 구조 분해 문법이라고도 불립니다.

// MyComponent.js

import React from 'react';

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

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

export default MyComponent;

함수 파라미터 부분에서도 사용할 수 있습니다. 만약 함수의 파라미터가 객체라면 그 값을 바로 비구조화해서 사용하는 것입니다.

// MyComponent.js

import React from 'react';

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

    );
};

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

export default MyComponent;

3.3.6 propTypes를 통한 props 검증

컴포넌트의 필수 props를 지정하거나 props의 타입을 지정할 때는 propTypes를 사용합니다. 컴포넌트의 propTypes를 지정하는 방법은 defaultProp을 설정하는 것과 비슷합니다. 우선 propTypes를 사용하려면 코드 상단에 import 구문을 사용하여 불러와야 합니다.

// MyComponent.js

import React from 'react';
import PropTypes from 'prop-types';

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

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

**MyComponent.propTypes = {
    name: PropTypes.string** // name 값은 무조건 문자열(string) 형태로 전달해야한다.
**}**

export default MyComponent;
// App.js

import MyComponent from './MyComponent';

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

export default App;

출력값

값이 나타나기는 했지만, 콘솔에 경고 메시지를 출력하여 개발자에게 propTypes가 잘못되었다는 것을 알려줍니다.

3.3.6.1 isRequired를 사용하여 필수 propTypes 설정

propTypes를 지정하지 않았을 때 경고 메시지를 띄우려면 propTypes를 지정할 때 뒤에 isRequired를 붙여 주면됩니다.

// MyComponent.js

import React from 'react';
import PropTypes from 'prop-types';

const MyComponent = ({ name, favoriteNumber, children }) => {
    return (
        <div>
            안녕하세요, 제 이름은 {name}입니다.<br />
            children 값은 {children}
            입니다.
            <br />
            제가 좋아하는 숫자는 {favoriteNumber}입니다.
        </div>
    );
};

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

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

export default MyComponent;
// App.js

import MyComponent from './MyComponent';

const App = () => {
  return <MyComponent name='React' favoriteNumber={4}> 리액트 </MyComponent>;
}

export default App;

출력값

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

클래스형 컴포넌트에서 props를 사용할 때는 render 함수에서 this.props를 조회하면 됩니다.

defaultProps와 propTypes는 똑같은 방식으로 설정할 수 있습니다.

import React, {Component} from 'react';
import PropTypes from 'prop-types';

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;

class 내부에서 지정하는 방법도 있습니다.

import React, {Component} from 'react';
import PropTypes from 'prop-types';

class MyComponent extends Component{
    static defaultProps = {
        name: '기본 이름'
    };

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

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

export default MyComponent;

3.4 State

리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미합니다. props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있습니다. props를 바꾸려면 부모 컴포넌트에서 바꿔야 합니다.

리액트에는 두 가지 종류의 state가 있습니다. 하나는 클래스형 컴포넌트가 지니고 있는 state이고, 하나는 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state입니다.

3.4.1 클래스형 컴포넌트의 state

// Counter.js

import React, { Component } from 'react';

class Counter extends Component {
    constructor(props){  // 컴포넌트 생성자 메서드
        super(props);    // constructor를 작성시 반드시 super(props) 호출 - 
                         // 호출 시 현재 클래스형 컴포넌트가 상속받고 있는 Component 클래스가 지닌 생성자 함수 호출        
        this.state = {   //state 초깃값 설정하기 - 객체 형식
            number:0,   
            fixedNumber:0
        };
    };

    render(){
        const {number , fixedNumber} = this.state // state를 조회할 때는 this.state로 조회합니다.
        return (
            <div>
                <h1>{number}</h1>
                <h2>바뀌지 않는 값: {fixedNumber}</h2>
                <button
                    onClick = {() =>{  // onClick을 props로 넣어줌 = 버튼 클릭시 호출 함수 설정 = 이벤트 설정 (이벤트로 설정할 함수를 넣어 줄 때는 화살표 함수 문법을 사용해야함)
                        // this.setState를 사용하여 state에 새로운 값을 넣을 수 있습니다.
                        this.setState({ number: number + 1 }); // 바뀌는 값만 인자 전달
                    }}
                >
                    +1
                </button>
            </div>
        );
    }
}

export default Counter;
// App.js

import Counter from './Counter';

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

export default App;

3.4.1.1. state를 constructor에서 꺼내기

앞에서 state의 초깃값을 지정하기 위해 constructor 메서드를 선언해 주었는데, 또 다른 방식으로도 state의 초깃값을 지정해 줄 수 있습니다.

import React, { Component } from 'react';

class Counter extends Component {
    **state = {
        number: 0,
        fixedNumber:0
    }**

    render(){
        const {number , fixedNumber} = this.state // state를 조회할 때는 this.state로 조회합니다.
        return (...);
    }
}

export default Counter;

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

this.setState를 사용하여 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트됩니다.

// Counter.js - button onClick 부분

onClick = {() => {
	// this.setState를 사용하여 state에 새로운 값을 넣을 수 있습니다.
	this.setState({ number: number + 1 });
	this.setState({ **number: this.state.number + 1** });
}}

코드를 위와 같이 작성하면 this.setState를 두 번 사용하는 것임에도 불구하고 버튼을 클릭할 때 숫자가 1씩 더해집니다. this.setState를 사용한다고 해서 state 값이 바로 바뀌지는 않기 때문입니다.

이에 대한 해결책은 this.setState를 사용할 때 객체 대신에 함수를 인자로 넣어 주는 것입니다.

this.setState의 인자로 함수를 넣어 줄 때는 코드를 다음과 같은 형식으로 작성합니다.

this.setState((prevState, props) => {
	return {
		// 업데이트하고 싶은 내용
	}
})

여기서 prevState는 기존 상태이고, props는 현재 지니고 있는 props를 가리킵니다. 만약 업데이트하는 과정에서 props가 필요하지 않다면 생략해도 됩니다.

// Counter.js - button

<button
	// onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정합니다.
	onClick={() => {
		this.setState(prevState => {
			return {
				number: prevState.number +1
			};
		});
		// 위 코드와 아래 코드는 완전히 똑같은 기능을 합니다.
		// 아래 코드는 함수에서 바로 객체를 반환한다는 의미입니다.
		this.setState(prevState => ({
			number: prevState.number + 1
		}));
	}}
>
	+1
</button>

이렇게 작성하면 숫자가 2씩 증가합니다.

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

setState를 사용하여 값을 업데이트하고 난 다음에 특정 작업을 하고 싶을 때는 setState의 두 번째 파라미터로 콜백(callback) 함수를 등록하여 작업을 처리할 수 있습니다.

// Counter.js - button

<button
  // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정합니다.
  onClick={() => {
    this.setState(
      {
        number: number + 1,
      },
      () => {
        console.log("방금 setState가 호출되었습니다.");
        console.log(this.state);
      }
    );
  }}
>
  +1
</button>

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

리액트 16.8 이전엔 함수형 컴포넌트에서 state를 사용할 수 없었습니다. 16.8 이후 useState라는 함수를 사용하여 함수형 컴포넌트에서도 state를 사용할 수 있게 되었습니다.

다만, 사용법이 조금 다릅니다 → 이 과정에서 Hooks 사용

3.4.2.1 배열 비구조화 할당

배열 비구조 할당: 배열 안에 들어 있는 값 쉽게 추출하도록 해주는 문법

const array = [1, 2];
const one = array[0];
const two = array[1];

// array 안에 있는 값을 one과 two에 담아주는 코드입니다.
// 위 코드는 배열 비구조화 할당을 사용하면 다음과 같이 표현할 수 있습니다.

const array = [1, 2];
const [one, two] = array;

3.4.2.2 useState 사용하기

클래스형 컴포넌트에서의 state 초깃값은 객체 형태를 넣어 주어야 하지만, useState에서는 값의 형태가 자유입니다.

// Say.js

import React, { **useState** } from 'react';

const Say = () => {
		// useState 함수의 인자에는 상태의 초깃값을 넣어 줍니다.
    const [message, setMessage] = useState('');   // 배열의 첫번째 원소: 현재 상태, 두번째 원소: 상태 바꿔주는 함수 = 세터(Setter) 함수
    const onClickEnter = () => setMessage('안녕하세요!');
    const onClickLeave = () => setMessage('안녕히 가세요.');

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

export default Say;
// App.js

import Say from './Say';

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

export default App;

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

// Say.js

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={{ color }}**>{message}</h1>
						<button style={{ color: 'red' }} onClick={() => setColor('red')}>
							빨간색
						</button>
						<button style={{ color: 'green' }} onClick={() => setColor('green')}>
							초록색
						</button>
						<button style={{ color: 'blue' }} onClick={() => setColor('blue')}>
							파란색
						</button>
       </div>
    );
    
};

export default Say;

3.5 state를 사용할 때 주의 사항

  • 클래스형 컴포넌트든 함수형 컴포넌트든 state값을 바꾸어야 할 떄는 setState 혹은 useState를 통해 전달받은 세터 함수를 사용해야 합니다.
  • 배열이나 객체를 업데이트 할 때는 객체 사본을 만들고 그 사본에 값을 업데이트한 후, 그 사본의 상태를 setState 혹은 세터 함수를 통해 업데이트 합니다.
     // 사본을 만들어서 업데이트하는 예시
    
    // 객체 다루기
    const object = {a: 1, b: 2, c: 3};
    const nextObject = {...object, b: 2}; // 사본을 만들어서 b 값만 덮어 쓰기
    
    // 배열 다루기
    const array = [
    	{ id: 1, value: true },
    	{ id: 2, value: true },
    	{ id: 3, value: false}
    ];
    let nextArray = array.concat({ id: 4 }); // 새 항목 추가
    nextArray.filter(item => item.id !== 2); // id가 2인 항목 제거
    nextArray.map(item => (item.id === 1 ? { ...item, value: false } : item)); // id가 1인 항목의 value를 false로 설정
    
    {/*
    	객체에 대한 사본을 만들 때는 spread 연산자라 불리는 ...을 사용하여 처리하고,
    	배열에 대한 사본을 만들 때는 배열의 내장 함수들을 활용합니다.
    *}

3.6 정리

  • 컴포넌트를 만들어서 내보내고 불러오는 방법 (export, import)
  • props 및 state를 사용하는 방법
  • props와 state는 둘 다 컴포넌트에서 사용하거나 렌더링할 데이터를 담고 있음
  • props는 부모 컴포넌트가 설정
  • state는 컴포넌트 자체적으로 지닌 값으로 컴포넌트 내부에서 값을 업데이트
  • 부모 컴포넌트의 state를 자식 컴포넌트의 props로 전달하고, 자식 컴포넌트에서 특정 이벤트가 발생할 때 부모 컴포넌트의 메서드를 호출하면 props도 유종적으로 사용 가능
  • 리액트 개발 팀이 함수형 컴포넌트와 Hooks를 사용하는 것이 주요 컴포넌트 개발 방식이 될 것이라고 공지했기 때문에 새로운 컴포넌트를 만들 때는 useState를 사용할 것을 권장
profile
디자이너인가 퍼블리셔인가 프론트엔드개발자인가 정체성의 혼란을 겪는 개린이

0개의 댓글