📗목차
컴포넌트를 선언하는 방식은 2가지이다.
import React from 'react';
import './App.css';
function App() {
const name = '리액트';
return <div className="react">{name}</div>;
}
export default App;
import React from 'react';
import './App.css';
class App extends Component {
render() {
const name = '리액트';
return <div className="react">{name}</div>;
}
}
export default App;
이 둘의 차이점은 클래스형 컴포넌트의 경우 state 기능 및 라이프사이클 기능을 사용할 수 있다는 것과 임의 메서드를 정의할 수 있다는 것이다.
클래스형 컴포넌트에서는 render 함수가 꼭 있어야 하고, 그 안에서 보여 주어야 할 JSX를 반환해야 된다.
컴포넌트를 선언할 수 있는 2가지 방법 중 어느 상황에 함수형 컴포넌트를 사용해야 될까?
함수형 컴포넌트의 장점을 나열해 보면 다음과 같다.
함수형 컴포넌트의 주요 단점
state와 라이프사이클 API 사용이 불가능하다는 점.
이 단점은 리액트 v16.8 업데이트 이후 Hooks라는 기능이 도입되면서 해결되었다. 클래스형 컴포넌트와 똑같이 사용할 수 있는 것은 아니지만 조금 다른 방식으로 비슷한 작업을 할 수 있게 되었다.
props는 properties를 줄인 표현으로 컴포넌트 속성을 설정할 때 사용하는 요소이다. props 값은 해당 컴포넌트를 불러와 사용하는 부모 컴포넌트에서 설정할 수 있다.
MyComponent 컴포넌트에서 name이라는 props를 렌더링하도록 설정해보겠다.
import React from 'react';
const MyComponent = props => {
return <div>안녕하세요, 제 이름은 {props.name}입니다.</div>;
};
export default MyComponent;
다음 아래 코드는 App 컴포넌트에서 MyComponent의 props 값을 지정한 것이다.
src/App.js
import React from 'react'; import MyComponent from './MyComponent'; const App = () => { return <MyComponent name="React"/>; } export default App;
이제 name 값을 지워 보겠다.
import React from 'react';
import MyComponent from './MyComponent';
const App = () => {
return <MyComponent/>;
}
export default App;
현재 name 값을 지정하지 않았기 때문에 브라우저에는 '안녕하세요, 제 이름입니다.' 라는 내용으로 출력된다.
지금처럼 props 값을 따로 지정하지 않았을 때 보여줄 기본값을 설정하는 defaultProps에 대해 알아보자.
import React from 'react';
const MyComponent = props => {
return <div>안녕하세요, 제 이름은 {props.name}입니다.</div>;
};
MyComponent.defaultProps = { name: '기본 이름' };
export default MyComponent;
컴포넌트 태그 사이의 내용을 보여 주는 props가 있는데, 이게 바로 children이다.
import React from 'react';
import MyComponent from './MyComponent';
const App = () => {
return <MyComponent>리액트</MyComponent>;
};
export default App;
import React from 'react';
const MyComponent = props => {
return (
<div>
안녕하세요, 제 이름은 {props.name}입니다. <br />
children 값은 {props.children} 입니다.
</div>
);
};
MyComponent.defaultProps = { name: '기본 이름' };
export default MyComponent;
현재 MyComponent에서 props 값을 조회할 때마다 props.name, props.children과 같이 props. 이라는 키워드 앞에 붙여 주고 있다.
이러한 작업을 더 편하게 하기 위해 ES6의 비구조화 할당 문법을 사용하여 내부 값을 바로 추출하는 방법을 알아보자.
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, children }) => {
return (
<div>
안녕하세요, 제 이름은 {name}입니다.<br />
children 값은 {children} 입니다.
</div>
);
};
MyComponent.defaultProps = { name: '기본 이름' };
export default MyComponent;
컴포넌트의 필수 props를 지정하거나 props의 타입을 지정할 때는 propTypes를 사용한다. 컴포넌트의 propTypes를 지정하는 방법은 defaultProps을 설정하는 것과 비슷하다. 우선 propTypes를 사용하려면 코드 상단에 import 구문을 사용하여 불러와야 한다.
import React from 'react';
import PropTypes from 'prop-types';
const MyComponent = ({ name, children }) => {
return (...); //코드 생략
};
MyComponent.defaultProps = {
name: '기본 이름'
};
MyComponent.propTypes = {
name: PropTypes.string
};
export default MyComponent;
이렇게 설정해주면 name 값은 무조건 문자열 형태로 전달해야 된다는 것을 의미한다.
propTypes를 지정하지 않았을 때 경고 메시지를 띄워 주는 작업을 해보자.
favoriteNumber라는 숫자를 필수 props로 지정해보겠다.
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;
이렇게 propTypes를 지정할 때 뒤에 isRequired를 붙여 주면 된다.
import React from 'react';
import MyComponent from './MyComponent';
const App = () => {
return <MyComponent name="React" favoriteNumber={7}>리액트</MyComponent>;
}
export default App;
클래스형 컴포넌트에서 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; // 그냥 props가 아님
return (
<div>
안녕하세요, 제 이름은 {name}입니다.<br/>
children 값은 {children} 입니다.<br/>
제가 좋아하는 숫자는 {favoriteNumber}입니다.<br/>
</div>
);
}
}
MyComponent.defaultProps = { name: '기본 이름' };
MyComponent.propTypes = {
name: PropTypes.string,
favoriteNumber: PropTypes.number.isRequired
}
export default MyComponent;
클래스형 컴포넌트에서 defaultProps와 propTypes를 설정할 때 class 내부에서 지정하는 방법도 있다.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class MyComponent extends Component {
static defaultProps = { name: '기본 이름' };
static propsTypes = {
name: PropTypes.string,
favoriteNumber: PropTypes.number.isRequired
};
render() {
const {name, favoriteNumber, children } = this.props;
return (...); // 생략
}
}
export default MyComponent;
💭 defaultProps와 propTypes는 꼭 사용해야 하나?
컴포넌트의 필수 사항이 아니므로 꼭 사용할 필요가 없다. 하지만 큰 규모의 프로젝트를 진행한다면, 특히 개발자들과 협업한다면 해당 컴포넌트에 어떤 props가 필요한지 쉽게 알 수 있어 개발 능률이 좋아질 것이다.
리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다.
props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있다.
리액트에서는 두 가지 종류의 state가 있다.
하나는 클래스형 컴포넌트가 지니고 있는 state이고,
다른 하나는 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state이다.
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
render() {
const { number } = this.state;
return (
<div>
<h1>{number}</h1>
<button
onClick={() => {
this.setState({ number: number + 1 });
}}
>
+1
</button>
</div>
);
}
}
export default Counter;
위 코드에서 각각 어떠한 역할을 하는지 알아보도록 하자.
컴포넌트에 state를 설정할 때는 다음과 같이 constructor 메서드를 작성하여 설정한다.
constructor(props) {
super(props);
this.state = { number: 0 };
}
이는 컴포넌트의 생성자 메서드인데, 클래스형 컴포넌트에서 constructor를 작성할 때는 반드시 super(props)를 호출해줘야 한다. 이 함수가 호출되면 현재 클래스형 컴포넌트가 상속하고 있는 리액트의 Component 클래스가 지닌 생성자 함수를 호출해준다.
그 다음에는 this.state 값에 초깃값을 설정해주었다. 컴포넌트의 state는 객체 형식이어야 한다.
이제 render 함수를 확인해보자.
render() {
const { number } = this.state;
return (
<div>
<h1>{number}</h1>
<button
onClick={() => {
this.setState({ number: number + 1 });
}}
>
+1
</button>
</div>
);
}
render 함수에서 현재 state를 조회할 때는 this.state를 조회하면 된다.
button 안에 onClick이라는 값을 props로 넣어 줬는데, 이는 버튼이 클릭될 때 호출시킬 함수를 설정할 수 있게 해준다. 이를 이벤트 설정한다고 한다.
이벤트로 설정할 함수를 넣어 줄 때는 화살표 함수 문법을 사용하여 넣어줘야 한다. 일반 함수는 이벤트가 호출될 때 실행되는 함수 안에서 "this"의 값이 컴포넌트 자기자신을 가르키지 않고 아무 값도 세팅되어 있지 않기 때문.
→ 🔗일반 함수 VS 화살표 함수 참고
함수 내부에서는 this.setState라는 함수를 사용했는데, 이 함수가 state 값을 바꿀 수 있게 해준다.
이제 Counter 컴포넌트를 App에 불러와 렌더링 해보자.
import React, { Component } from 'react';
import Counter from './Counter';
class App extends Component {
render() {
return <Counter />
}
}
export default App;
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
number: 0,
fixNumber: 0
};
}
render() {
const { number, fixNumber } = this.state;
return (
<div>
<h1>{number}</h1>
<h2>바뀌지 않는 값: {fixNumber}</h2>
<button
onClick={() => {
this.setState({ number: number + 1 });
}}
>
+1
</button>
</div>
);
}
}
export default Counter;
state의 초깃값을 지정하기 위해 constructor 메서드를 선언해줬는데, 다른 방식으로도 state의 초깃값을 지정해줄 수 있다.
import React, { Component } from 'react';
class Counter extends Component {
state = {
number: 0,
fixNumber: 0
};
render() {
const { number, fixNumber } = this.state;
return (...);
export default Counter;
이렇게 하면 constructor 메서드를 선언하지 않고도 state 초깃값을 설정할 수 있다.
this.setState를 사용하여 state 값을 업데이트 할 때는 상태가 비동기적으로 업데이트 된다. 만약 다음과 같이 onClick에 설정한 함수 내부에서 this.setState를 두번 호출하면 어떻게 될까?
Counter.js - button onClick
onClick{() => { this.setState({ number: number + 1 }); this.setState({ number: this.state.number + 1 }); }}
위와 같이 작성하면 this.setState를 두번 사용하는 것임에도 불구하고 버튼을 클릭할 때 숫자가 1씩 더해진다. this.setState를 사용한다고 해서 state 값이 바로 바뀌지 않기 때문이다.
이에 대한 해결책은 this.setState를 사용할 때 객체 대신에 함수를 인자로 넣어 주는 것이다. 다음 코드를 확인해보자.
this.setState((prevState, props) => {
return {
// 업데이트 하고 싶은 내용
}
})
여기서 prevState는 기존 상태이고, props는 현재 지니고 있는 props를 가리킨다. 만약 업데이트 하는 과정에서 props가 필요하지 않다면 생략해도 된다.
<button
onClick={() => {
this.setState(prevState => {
return {
number: prevState.number + 1
};
});
// 위 코드와 아래 코드는 완전히 똑같은 기능을 한다.
// 아래 코드는 함수에서 바로 객체를 반환한다는 의미이다.
this.setState(prevState => ({
number: prevState.number + 1
}));
}}
>
+1
</button>
onClick에서 두 번째로 this.setState 함수를 사용할 때는 화살표 함수에서 바로 객체를 반환하도록 했기 때문에 prevState => ({ })와 같은 형태로 코드가 이루어진다.
브라우저에서 버튼을 눌러보자 숫자가 2씩 올라가는가?
setState를 사용하여 값을 업데이트 하고 난 다음 특정 작업을 하고 싶을 때는 setState의 두번째 파라미터로 콜백함수를 등록하여 작업을 처리할 수 있다.
<bubtton
onClick={() => {
this.setState(
{
number: number + 1
},
() => {
console.log("방금 setState가 호출되었습니다.");
console.log(this.state);
}
);
}}
>
+1
</button>
리액트 16.8 이전 버전에서는 함수형 컴포넌트에서 state를 사용할 수 없었다. 하지만 16.8 이후부터는 useState라는 함수를 사용하여 함수형 컴포넌트에서도 state를 사용할 수 있게 되었다.
배열 비구조화 할당은 이전에 배운 객체 비구조화 할당과 비슷하다.
즉, 배열 안에 들어 있는 값을 쉽게 추출할 수 이도록 해주는 문법이다.
다음 코드를 살펴보자.
const arr = [1, 2];
const one = arr[0];
const two = arr[1];
이 코드를 배열 비구조화 할당을 사용하여 수정해보자.
const arr = [1, 2];
const [one, two] = arr;
훨씬 깔끔해졌다.
src/Say.js
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;
src/App.js
const App = () => { return <Say />; }; export default App;
useState 함수의 인자에는 상태의 초깃값을 넣어준다.
위에서 클래스형 컴포넌트에서의 state 초깃값은 객체 형태를 넣어 줘야 한다고 배웠다.
참고) 컴포넌트의 state는 객체 형식이어야 한다.
useState에서는 반드시 객체가 아니어도 상관 없다. 값의 형태는 자유이다. 숫자일 수도, 문자열일 수도, 객체일 수도, 배열일 수도 있다.
useState 함수를 호출하면 배열이 반환되는데, 배열의 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 바꾸어 주는 함수이다. 이 함수를 세터(Setter) 함수라고 부른다.
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;
props와 state는 둘 다 컴포넌트에서 사용하거나 렌더링할 데이터를 담고 있으므로 비슷해 보일 수 있지만, 그 역할은 매우 다르다. props는 부모 컴포넌트가 설정하고, state는 컴포넌트 자체적으로 지닌 값으로 컴포넌트 내부에서 값을 업데이트할 수 있다.
props를 사용한다고 해서 값이 무조건 고정적이지 않다. 부모 컴포넌트의 state를 자식 컴포넌트의 props로 전달하고, 자식 컴포넌트에서 특정 이벤트가 발생할 때 부모 컴포넌트의 메서드를 호출하면 props도 유동적으로 사용할 수 있다.
클래스형 컴포넌트의 state와 함수형 컴포넌트의 useState에 대해 배워 보았다. 앞으로 새로운 컴포넌트를 만들 때는 useState를 사용할 것을 권장한다.
이로써 코드가 더 간결해질 뿐만 아니라, 리액트 개발 팀이 함수형 컴포넌트와 Hooks를 사용하는 것이 주요 컴포넌트 개발 방식이 될 것이라고 공지했기 때문이다.
참고문헌
김민준,「리액트를 다루는 기술 :실무에서 알아야 할 기술은 따로 있다!」, 길벗, 개정판[실은 2판] 2019 (개정판)