이재승님의 '실전리액트 프로그래밍 리액트 훅부터 Next.JS까지'(출판사: 프로그래밍인사이트)의 4장을 읽고 정리한 내용입니다.
본격적으로 리액트를 활용하여 웹을 개발하기 전에 컴포넌트를 효율적이고 올바르게 작성하고싶었다. 오늘은 컴포넌트 작성법을 공부해본 뒤 메인페이지 개발에 들어가 볼 것이다.
대부분의 작업이 이루어지는 컴포넌트 파일이 정리가 잘 안 되어있고 수정할 때 마다 코드를 찾아다녀야 한다면 불필요하게 에너지와 시간을 낭비하게 된다.
일반적으로
1. 비슷한 종류의 코드를 한 곳으로 모으기
2. 그룹 간의 우선순위를 정한 뒤 중요한 그룹의 코드를 파일 위쪽으로 올려주기
추천하는 컴포넌트 파일 작성 순서
클래스형 컴포넌트 작성하기
class MyComponent extends React.Component {
static propTypes = { };//1️⃣
state = { }; //2️⃣
constructor(props) { } //┓
componentDidMount() { } //3️⃣
componentWillUnmount { } //┛
requestData() { } //┓
onclick = ()=>{ } //┛4️⃣
render() { //5️⃣
const {prop1, prop2 } = this.props;
const {state1, state2 } = this.state;
// ...
}
}
const URL_PRODUCT_LIST = '/api/products'; //┓
function getTotlaPrice({ price, total }) { } //┛6️⃣
export default Mycomponent;
1️⃣
: 파일의 최 상단에는 속성값의 파입을 정의한다. 컴포넌트 사용자의 입장에서 해당 컴포넌트 파일을 열었을 때 속성값이 가장 먼저 보이는 것이 좋다. 작성자의 입장에서도 속성값이 중요하기 때문에 속성값 위쪽으로는 import코드만 오도록 작성하는 것이 좋다.
2️⃣
: state의 초기화 코드를 작성한다. 메서드들 사이에 위치하는 것 보다 데이터를 한곳에 모은 뒤 사용하는 것이 가독성에 좋다.
3️⃣
: 우선 생성자를 정의한 후 render메서드를 제외한 리액트의 생명 주기 메서드를 정의한다. 생명주기 메서드를 많이 사용하는 컴포넌트의 경우라면 생명주기 메서드만 모아놓아도 화면이 한가득 채워지기도 한다.
4️⃣
: render메서드를 제외한 나머지 메서드를 정의한다. 로직을 분리하거나 반복해서 사용 될 메서드, 이벤트 처리 함수 등이 해당한다. 만약 생명주기 메서드가 아니면서 this객체가 필요 없다면 클래스의 바깥으로 빼는게 좋다. 이렇게 하면 코드의 복잡도가 낮아지고 테스트 코드를 작성하기도 용이해진다.
5️⃣
: 두 줄의 코드를 보면 메서드 내부에서 사용할 props와 state를 객체 비구조화를 사용해 풀어놓았다. 이 방법을 사용하면 메서드 내부에서 this.prop.~ , this.state.~ 코드를 반복하지 않아도 된다. render메서드 뿐 아니라 모든 메서드에서 이렇게 활용이 가능하다.
6️⃣
: 컴포넌트 바깥의 변수와 함수는 파일의 가장 밑에 정의한다. 특별히 이유가 없다면 const로 정의하는 것이 좋다고 한다.(왜일깡)
-> 컴포넌트 외부에서 정의된 상수 변수를 사용하는 코드
class MyComponent extends Component {
//...
render () {
return <table columns={COLUMNS} />;
}
}
const COLUMNS = [ //1️⃣
{ id: 1, name: 'phoneNumber', width: 200, color: 'white' },
{ id: 1, name: 'city', width: 100, color: 'grey' },
//...
];
만약 render 내부에서 커다란 객체를 생성하는 코드가 있다면 1️⃣처럼 컴포넌트 외부에서 정의하는 상수 변수로 정의해주는 것이 좋다. 만약 COLUMNS 상수 변수를 사용하지 않고 render메서드 내부에서 배열 리터럴로 직접 작성했다면 가독성 뿐 아니라 render메서드가 호출될 때 마다 크기가 큰 객체가 계속 생성되므로 성능이 낮아진다. 빈번하게 값이 변하지만 UI에는 영향을 안주는 값이라면 재할당 가능한 변수인 let도 괜찮다.
함수형 컴포넌트 작성하기
함수형 컴포넌트는 기능이 단순하기 때문에 클래스형 컴포넌트보다 가독성이 좋다. 따라서 클래스형 컴포넌트가 꼭 필요한 경우가 아니라면 함수형 컴포넌트의 작성을 권장한다!
MyComponent.propTypes = {
//...1️⃣
};
function MyComponent({prop1, prop2}) {
//...2️⃣
}
const URL_PRODUCT_LIST = '/api/products';//...┓3️⃣
function getTotalPrice({ price, total }) { //┛
}
export default MyComponent; //4️⃣
1️⃣
: 파일의 상단에서 속성값 타입 정의
2️⃣
: 함수형 컴포넌트의 매개변수는 명명된 매개변수로 정의하는 것이 좋다.
3️⃣
: 컴포넌트에서 사용되는 변수와 함수를 컴포넌트 코드 아래에 작성한다.
4️⃣
: 컴포넌트를 외부로 내보내는 코드는 파일의 가장 아래에 작성한다. 이렇게 하면 컴포넌트에 이름을 부여하도록 강제할 수 있다.
-> 이름없는 컴포넌트가 되는 예: export default function() { }
혹은 export default ()=>{ }
prop-types는 속성값의 타입 정보를 정의할 때 사용하는 리액트 공식 패키지다.
자바스크립트는 동적 타입의 언어이다. 타입이 정해져있지 않기 때문에 배우기 쉽고 간단한 프로그램을 작성하는 것에 있어서 생산성이 매우 좋지만 대규모의 프로그램을 작성할 때는 오히려 생산성이 떨어져 이럴 땐 정적 타입의 언어를 사용하는 것이 좋다 (타입스크립트)
타입스크립트를 사용한다면 proptypes를 사용하지 않고도 컴포넌트 간 주고받는 데이터의 타입을 지정해줄 수 있다.
하지만 동적 타입의 언어를 사용하는 것이 더 생산성이 높거나 꼭 사용해야만 하는 순간이 있다. 이럴 땐 동적타입언어의 단점인 타입 정의에 대한 부분을 보완할 수 있는 prop-types패키지를 사용하는 것이다.
이것은 필수요소는 아니지만 가독성, 다른 언어에 비해 상대적으로 자료형에 자유도가 높은 자바스크립트를 사용하면서 발생하는 에러상황이나 컴포넌트가 깊어질수록 props의 관리가 어려워지는 상황을 방지하기 위해 사용하는 것이 좋다. (typescript쓰면 해결될 일이긴 함요요)
prop-types 패키지를 사용하는 경우 컴포넌트 사용시 props에 잘못된 타입이 입력되면 콘솔에 에러메시지를 출력시킨다. 물론 해당 검사는 별도의 연산이 필요하므로 개발 모드에서만 작동한다. 따라서 타입 오류를 사전에 검사하기 위한 용도로 쓰인다.
이외에도 타입 정의 자체가 컴포넌트를 사용하는 사용자 입장에서 훌륭한 문서가 될 수 있기 때문에 동적타입의 언어를 사용하는 경우에는 속성값 타입을 정의해주도록 하자.
우선 패키지 설치가 필요하다.
나는 yarn을 설치했기 때문에 yarn add prop-types
npm을 사용한다면 npm i --save prop-types
prop-types를 이용한 props의 타입 정보
class MyComponent extends Component {
static propTypes = {
name: PropTypes.string.isRequired, 1️⃣
title: PropTypes.string,2️⃣
age: PropTypes.number,3️⃣
editable: PropTypes.bool,
onChangeName: PropTypes.func,4️⃣
onChangeTitle: PropTypes.func.isRequired,
};
}
1️⃣
: name
은 필수값이기 때문에 부모 컴포넌트에서 이 값을 주지 않으면 에러메시지가 출력된다.
2️⃣
: title
은 필수값이 아니기 때문에 값이 내려오지 않아도 에러가 발생하지 않는다.
3️⃣
: age
의 값으로 문자열을 줬다면 타입이 잘못됐다는 에러 메시지가 출력된다.
4️⃣
: onChangeName
는 함수이다. prop-types에서 함수의 매개변수와 반환값에 대한 타입 정보는 확인할 수 없으므로 주석으로 자세한 타입 정보를 적어주는 것을 추천한다.
prop-types로 정의할 수 있는 타입
import PropTypes from 'prop-types';
class MyComponent extends React.Component {
static propTypes = {
menu: PropTypes.element,
//리액트 요소
//<div>hello</div> 🆗
//<SomeComponent /> 🆗
//123 ❌
description: PropTypes.node,
//렌더 함수가 리턴할 수 있는 모든 것
//number, string, array, element ...
//<someComponent /> 🆗
//123 🆗
message: PropTypes.instanceOf(Message),
//Message 클래스로 생성된 모든 객체
//new Message() 🆗
//new Car() ❌
name: PropTypes.oneOf(['jone', 'mike']),
//배열에 포함된 값 중 하나를 만족
//'jone' 🆗
//'messy' ❌
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
//배열에 포함된 타입 중에서 하나를 만족
//123 🆗
//'messy' ❌
ages: PropTypes.arrayOf(PropTypes.number),
//특정 타입만 포함하는 배열
//[1, 2, 3] 🆗
//['a', 'b'] ❌
info: PropTypes.shape({
color: PropTypes.string,
weight: PropTypes.number,
}),
//객체의 속성값 타입 정의
//{color: 'red', weight: 123} 🆗
//{color: 'red', 'weight: '123kg'} ❌
infos: PropTypes.objectOf(PropTypes.number),
///객체에서 모든 속성값의 타입이 같은 경우
//{prop1: 123, prop2: 456} 🆗
//{prop1: 'red', prop2: 456} ❌
};
}
함수를 이용해 prop-types 커스텀하기
class MyComponent extends Component {
static propTypes = {
age: function(props, propName, componentName) {
const value = props[propName];
if (value < 10 || value > 20) {
return new Error(
'Invalid prop ${propName} supplied to ${componentName}. It must be 10 <= value <= 20 !',
);
}
},
};
// ...
}