<리액트를 다루는 기술> 3장 컴포넌트
07 Jul 2020 | javascript react componentvelopert님의 <리액트를 다루는="" 기술=""> 개정판을 보고 공부한 것을 정리합니다.리액트를>
- 리액트에서는 ‘컴포넌트’라는 구성단위로 프론트 개발을 하게 된다.
- 컴포넌트: 구성요소
- 화면을 구성하는 요소들을 각각 만들고 그것을 합쳐 하나의 페이지가 되는 형식
- 이 컴포넌트 또한 작게 쪼개서 작은 부분을 합쳐 하나의 컴포넌트로 만들 수 있다. (하나의 큰 컴포넌트는 다른 작은 컴포넌트들의 조합이다, 라는 점이 함수형 프로그래밍을 생각나게 하는듯….)
- 하나의 컴포넌트 안에서 쪼개고 쪼개기를 반복하면 하나의 작은 tree 구조처럼 될 수 있다. 큰 요소 안에 작은 요소가 있으니 부모 자식의 관계가 형성될 수 있다.
클래스형 컴포넌트
리액트를 사용하여 애플리케이션의 인터페이스를 설계할 때 사용자가 볼 수 있는 요소는 여러 가지 컴포넌트로 구성되어 있다.
뒤에서 만들어 볼 일정 관리 애플리케이션에 사용된 컴포넌트
- TodoTemplate: 현재 화면의 중앙에 있는 사각형 레이아웃 컴포넌트
- TodoInput: 새로운 항목을 추가할 수 있는 컴포넌트
- TodoList: 할 일 항목을 여러 개 보여주는 컴포넌트
- 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를 반환해야 한다.
함수형 컴포넌트의 장점
- 클래스형 컴포넌트보다 선언하기가 훨씬 편하다.
- 메모리 자원도 클래스형 컴포넌트보다 덜 사용한다.
- 프로젝트를 완성하여 빌드한 후 배포할 때도 함수형 컴포넌트를 사용하는 것이 결과물의 파일 크기가 더 작다. (아주 적은 차이)
함수형 컴포넌트의 단점
state와 라이프사이클 API의 사용이 불가능하다
-> 리액트 v16.8 업데이트 이후 Hooks
라는 기능이 도입되면서 해결됨 완전히 클래스형 컴포넌트와 똑같이 사용할 수 있는 것은 아니지만 조금 다른 방식으로 비슷한 작업을 할 수 있다.
리액트 공식 매뉴얼에서는 컴포넌트를 새로 작성할 때 함수형 컴포넌트와 Hooks를 사용하도록 권장.
첫 컴포넌트 생성
- 파일 만들기
- 코드 작성하기
- 코듈 내보내기 및 불러오기
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를 통해 값을 전달한다.
- 클래스형 컴포넌트의 state
- 함수형 컴포넌트에서
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
사용 시 생성자 내에서 정의되지 않아 버그 발생 가능성이 생긴다.
- super(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()
를 사용
- 외부 API 호출이 필요하다면 생성자 밖에서
state
에props
를 복사하면 안 된다.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를 사용하는 것이 주요 컴포넌트 개발 방식이 될 것이라고 공지
참고
- React props와 state 개념 이해하기
- 비구조화 할당 (구조분해) 문법
- Prop types 종류
- constructor() 생성자 사용
- React 공식 문서 - React.Component
- React.js의 state 사용방법