이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.
목차
1. What is Component?
2. Stateless Functional Component / Class Component
3. Create Component (ES6)
4. State
1. What is Component?
리액트로 만든 앱을 구성하는 최소단위.
비록 자바스크립트로 만든 Todolist지만 이걸 리액트로 만들고자 한다면 컴포넌트를 어떻게 구성할 수 있을까
실시간으로 시간을 보여주는 TodoTitle, 새로운 항목을 추가한 TodoInput, 할 일을 보여줄 TodoList 등의
여러가지 요소들로 나눌 수 있다.
각각의 컴포넌트는 단순히 템플릿의 기능으로 끝나지 않는다.
props와 state에 따라 UI를 만들어주거나, 임의의 메서드를 정의하여 해당 컴포넌트만의 기능을 구현할 수도 있다.
2. Stateless Functional Component / Class Component
컴포넌트에는 함수형 컴포넌트(Stateless Functional Component)와 클래스 컴포넌트(Class Component)가 있다.
함수형 컴포넌트는 리액트를 설치했을 때, 제일 처음 App.js에 작성되어 있는 코드를 떠올리면 된다.
import './App.css';
function App() {
return (
<div>
HELLO!
</div>
);
}
export default App;
여기서 export default는 현재 작성한 컴포넌트를 import하는 파일에서 불러올 수 있도록 정의해주어야 한다.
클래스형 컴포넌트는 위의 코드를 아래처럼 작성한 것이다.
import './App.css';
import { Component } from 'react';
class App extends Component {
render() {
return (
<div>
HELLO!
</div>
)
}
}
export default App;
오히려 더 복잡해지고 쓸모 없어진 것 같은데..? 실은 그렇지 않다.
클래스 컴포넌트를 사용하면 아직 배우지 않은 state와 LifeCycle 기능을 사용하고 임의의 메서드를 정의할 수 있게 된다.
import './App.css';
import { Component } from 'react';
class App extends Component {
constructor(props) { // 생성자
super(props);
this.props = props;
}
componentDidMount() { // 생명주기함수 override
}
say() {
console.log(this.props + ': 안녕?');
}
render() { // render 함수 override. 필수로 정의해주어야 함.
return <div>HELLO!</div>
}
}
export default App;
Python이나 Java같은 객체 지향 언어를 다뤄본 경험이 있다면 어려운 내용이 아닐 것이다.
언어를 다루는 포스팅은 아니므로 기초적인 내용은 생략한다.
중요한 것은 클래스형 컴포넌트에는 JSX를 반환하는 render 함수는 꼭 정의해주어야 한다는 것.
함수형 컴포넌트가 메모리 자원도 덜 차지하고 편리성과 가독성도 더 좋지만 state와 LifeCycle API사용이 불가능 했었다.
그런데 이 단점을 Hooks라는 기능이 도입되면서 해결되었는데 완전히 똑같이 사용할 수 있는 건 아니지만 다른 방식을 통해서 비슷한 작업을 수행하도록 구현할 수 있게 되었다.
Hooks는 나중에 따로 포스팅을 다룰 정도로 중요한 내용이라서 간략하게만 언급하자면
클래스형 컴포넌트의 문제점은 관심사 분리가 제대로 되지 않고 컴포넌트간 중복이 너무 많아져서
규모가 큰 컴포넌트가 만들어지게 되었고(애초 리액트 개발 목적에 부합하지 않음) 유지보수가 힘들어졌다.
처음에는 HOC를 이용해 컴포넌트를 쪼개어 재사용하고자 했으나 클래스의 this 키워드의 동작방식이
예측 불가능하게 작동하는 경우가 많아서 오류가 발생했다.
결국 이 모든 문제가 함수형 컴포넌트가 state를 가지지 못 하기 때문이라고 판단하여 Hooks가 탄생하게 되었다.
그렇다면 클래스 컴포넌트를 써야 하는가? 함수형 컴포넌트와 Hooks를 써야 하는가?
리액트 공식 메뉴얼에 의하면 컴포넌트를 새로 작성할 때 후자의 방법을 권장하고 있다.
하지만 클래스 컴포넌트도 알아야 한다. 내가 후자의 방법을 쓴다고 해서 모든 개발자가 그렇게 하지는 않으니까 ^^
3. Create Component (ES6)
직접 컴포넌트를 하나 만들어보자.
src 디렉토리에 component 디렉토리를 만들고 Contents.js를 만들어주었다.
App.js에서 해당 컴포넌트를 import 하여 연결시켜준다.
import './App.css';
import Contents from './component/Contents'
function App() {
return (
<Contents />
);
}
export default App;
Components.js는 다음과 같이 작성한다.
export default function Contents() {
return (<div>방법은 다양하다~</div>)
}
const Contents = () => {
return (<div>방법은 다양하다~</div>)
}
export default Contents
위랑 아래랑 결과는 같다.
이건 JavaScript EX6에서 도입된 화살표 함수 때문인데 혹시나 이렇게 선언되어 있는 함수를 봐도 당황하지 말자.
여기까지 했으면 리액트 앱 화면에 "방법은 다양하다~"라는 내용의 div 태그가 추가되었을 것이다.
이번엔 컴포넌트에 데이터(props)를 전달해보자.
1. props
properties(속성)을 의미한다.
(...)
<Contents test="Test 화면입니다."/>
(...)
const Contents = (props) => {
return (<div>ㅎㅎ~ {props.test}</div>)
}
export default Contents
이렇게 하면 해당 컴포넌트에게 props를 전달할 수 있다.
props는 객체로 보내지기 때문에 여러개의 속성을 전달할 수도 있다.
문제는 props가 제대로 전달이 안 되면 "ㅎㅎ~"밖에 출력이 되지 않기 때문에 props가 Null인 경우를 방지하고자
default 값을 설정할 수 있다.
Contents.defaultProps = {
test: "아무말이나 해봐"
}
선언된 Contents 함수 아래에 정의해주면 된다.
props를 이용하면 재밌는 기능을 구현할 수 있는데 Component에 둘러쌓인 문자열이 있는 경우를 보자.
import './App.css';
import Contents from './component/Contents'
function App() {
return (
<Contents>내가 어떻게 보이나요?</Contents>
);
}
export default App;
이 상태로 리액트를 실행시켜도 사이의 문구는 렌더링되지 않는다.
const Contents = (props) => {
return (<div>
ㅎㅎ~ {props.test} <br />
children 내용은 {props.children} 였습니다~
</div>
)
}
Contents.defaultProps = {
test: "아무말이나 해봐"
}
export default Contents
그런데 Contents에서 props.children 값을 직접적으로 끄집어내서 JSX로 보내버리면 렌더링 된다.
2. props 비구조화 할당
위에서 언급했듯이 props는 객체다.
그렇기 때문에 매번 사용할때마다 props.(property)로 사용하지 않고 비구조화 시켜 변수에 할당할 수 있다.
const Contents = (props) => {
const { test, children } = props
return (<div>
ㅎㅎ~ {test} <br />
children 내용은 {children} 였습니다~
</div>
)
}
Contents.defaultProps = {
test: "아무말이나 해봐"
}
export default Contents
이런 방법을 비구조화 할당 또는 구조 분해 문법이라고 부른다.
애초에 처음부터 props가 아니라 { test, children}으로 인자를 받을 수도 있다.
3. propTypes
import PropTypes from 'prop-types';
const Contents = ({ test, children }) => {
(...)
컴포넌트 필수 props를 지정하거나 타입을 지정할 때 사용한다.
.defaultProps와 비슷하게 설정하면 된다. 우선, import 구문으로 propTypes를 불러온다.
그리고 하단에 이렇게 코드를 추가해준다.
Contents.propTypes = {
test: PropTypes.string
}
이렇게 되면 props의 test로 들어오는 값은 무조건 string 타입이어야 함을 의미한다.
만약 인자값을 숫자로 던져주면 값이 나타나긴 하는데 타입이 잘못되었다고 콘솔탭에서 경고창이 뜬다..
이번에는 isRequired를 이용해서 필수로 설정해야하는 propTypes에 대해서 명시해주자.
import PropTypes from 'prop-types';
const Contents = ({ test, nbr, children }) => {
return (<div>
ㅎㅎ~ {test} <br />
children 내용은 {children} 였습니다~ <br />
숫자 : {nbr}
</div>
)
}
(...)
Contents.propTypes = {
test: PropTypes.string,
nbr: PropTypes.number.isRequired
}
(...)
이렇게 하면 nbr값을 App.js에서 넘겨주지 않았기 때문에 또한 경고가 나타난다.
이외에도 array, func, bool, object 등등 다양한 종류의 propTypes가 존재한다.
4. 클래스형 컴포넌트에서 props 사용
import PropTypes from 'prop-types';
import { Component } from 'react';
class Contents extends Component {
render() {
const { test, nbr, children } = this.props;
return (
<div>
ㅎㅎ~ {test} <br />
children 내용은 {children} 였습니다~ <br />
숫자 : {nbr}
</div>
);
}
}
(...)
this를 이용해서 조회하면 된다.
나머지는 함수형 컴포넌트에서 사용하던 방법과 동일하다.
defaultProps와 propTypes를 클래스 내부에서 지정할 수도 있다.
class Contents extends Component {
static defaultProps = {
test: "아무말이나 해봐"
};
static propTypes = {
test: PropTypes.string,
nbr: PropTypes.number.isRequired
}
render() {
const { test, nbr, children } = this.props;
return (
<div>
ㅎㅎ~ {test} <br />
children 내용은 {children} 였습니다~ <br />
숫자 : {nbr}
</div>
);
}
}
4. State
props는 부모 컴포넌트가 자식에게 던져주는 설정값이기 때문에 자식 컴포넌트가 수정할 수 없다.
반면에 state란 컴포넌트에서 바뀔 수 있는 값을 의미하여, 리액트에선 변수를 보통 state로 다룬다.
함수형 컴포넌트에서는 useState라는 Hooks를 이용하여 state를 관리할 수 있다.
보통 props가 가져온 값과 함께 가공시켜 사용한다.
props는 외부자를 위한 데이터고, state는 내부자를 위한 데이터라고 생각하면 편하다.
1. 클래스 컴포넌트
솔직히 클래스 컴포넌트 정말 써본 적도 없고, 쓸 일도 잘 없어서 건너뛰고 싶다. ㅎㅎ
하지만 해야겠지..
import { Component } from 'react';
class Contents extends Component {
constructor(props) {
super(props);
this.state = {nb: 0};
}
render() {
const { nb } = this.state;
return (
<div>
<h2>{nb}</h2>
<button onClick={() => this.setState({ nb: nb+1})}>눌러!</button>
</div>
)
}
}
export default Contents
this.state에서 state의 초기값을 설정해두었다가 버튼을 클릭하면 this.setState함수가 state의 값을 변경한다.
import { Component } from 'react';
class Contents extends Component {
constructor(props) {
super(props);
this.state = {
nb: 0,
str: "hello"
};
}
render() {
const { nb, str } = this.state;
return (
<div>
<h1>{str}</h1>
<h2>{nb}</h2>
<button onClick={() => this.setState({ nb: nb+1})}>눌러!</button>
</div>
)
}
}
export default Contents
state 내에 값이 여러개 있어도 상관없다.
str은 값을 변경하지 않을 건데 굳이 setState함수에 str까지 고려해주어야 할 필요는 없다.
this.setState 함수는 인자로 전달된 객체만 바꾸는 역할이므로 꼭 선언된 모든 state를 보낼 필요는 없다.
constructor(생성자)를 선언하지 않고도 state = { nb: 0 }; 을 렌더 함수 이전에 선언해주면
똑같이 사용할 수 있다.
또 재밌는 기능이 하나 있는데 state를 update하고 난 후에 setState함수에 파라미터를 하나 더 넘겨서
추가적인 작업을 수행하게 만들 수 있다.
(...)
<button onClick={() => this.setState({ nb: nb+1}, ()=>{
console.log("값이 업데이트 되었습니다!")
})}>눌러!</button>
(...)
2. 함수형 컴포넌트
※ 참고
리액트 16.8 이전까지는 함수형 컴포넌트는 state를 관리하는 것이 불가능했다.
하지만 클래스형 컴포넌트의 단점이 부각되면서 이에 대한 해결 방안이 필요했고
Hooks라는 기능이 도입되면서 useState를 활용해 함수형 컴포넌트에서도 state를 관리할 수 있게 되었다.
우선, useState를 사용하려면 useState가 어떤 값을 반환하는지부터 알아야 한다.
console.log(useState('Hello'));
객체를 반환하는데 0번에는 초기값으로 던져준 'Hello'가 저장되어있고, 1번에는 어떤 함수가 주어져있다.
이 함수가 바로 state를 컨트롤 할 수 있게끔 도와주는 함수이다.
그런데 대체 리액트에서는 왜 "변수 = 값"을 이용해서 변경하는 것이 아니라 useState를 쓰는 것일까?
사실 조금 어려울 수 있는 내용이라 나중에 Hooks을 집중적으로 다룰 때 다시 언급할 것이지만 간단히 말해서
함수로써 state를 바꾸지 않으면 리액트는 state의 값이 변경되었음을 인지하지 못 한다.
즉, 일반 변수를 수정한다고 해서 리액트에서 그것까지 바로바로 렌더링을 해주지는 않는다는 말이 된다.
update를 통해서 바로바로 렌더링을 해주어야 하는데, 변경 사항을 인지하지 못 하니
사용자가 직접 새로고침을 하기 전까지 화면에 표시되는 결과가 바뀌지 않는 불상사가 일어난다..
import { useState } from 'react';
const Contents = () => {
const [mode, setMode] = useState('');
return (
<div>
<button onClick={() => {setMode('Welcome!')}}>1번</button>
<button onClick={() => {setMode('Bye!')}}>2번</button>
<h2>{mode}</h2>
</div>
)
}
export default Contents
배열 비구조화 할당을 통해서 userState 객체를 [변수, setter 함수]로 받았다.
setMode("value") => 이런 식으로 mode의 값을 수정할 수 있다.
확인해보면 바로바로 결과화면이 바뀌는 것을 알 수있다.
이걸 useState를 쓰지 않고 직접 변수를 고쳐서 실행해보면 값의 변화가 DOM에 반영되지 않는다.
(구현해내려면 할 수는 있는데 그걸 목적으로 한 설명이 아니므로 넘어가도록 하자.)
state를 사용할 때, 0번째 인자로 반드시 변수가 올 것이라는 보장이 없다.
배열이나 객체가 올 수도 있는데 이럴 때는 보통 배열과 객체 사본을 만들어 사본에 업데이트한 후,
그 사본의 상태를 setter 함수에 넘기는 방법을 택한다.
이때 특히 spread operator(전개 연산자)에 대해 숙지해두어야 코딩이 편해진다.
const arr1 = ['one', 'two'];
const arr2 = ['three', 'four'];
let arr3 = [ arr1[0], arr1[1], arr2[0], arr[1] ]; // 과거
arr3 = [ ...arr1, ...arr2 ]; // ES6
const [ one, two, three = 'empty', ...others ] = arr1;
// one = 'one' , two = 'two', three = 'empty' , others = []
// 객체
let obj1 = { one : 1, two : 2, other : 0 };
let obj2 = { three : 3, four : 4, other : -1 };
let comb = { ...obj1, ... obj2 };
// comb = { one : 1, two : 2, three : 3, four : 4, other : -1 };
comb = { ...obj2, ...obj1 };
// comb = [ one : 1, two : 2, three : 3, four : 4, other : 0 };
let { other , others } = comb;
// other = 0
// others = { one : 1, two : 2, three : 3, four : 4 }