리액트를 배우기 시작한 레벨2의 첫 미션은 <리액트 로또 미션>이다. ✨럭키✨하게도 지난 레벨에서 <로또 미션>에서도 리뷰를 도와주셨던 토스페이먼츠의 @Jbee 님께 또 피드백을 받을 수 있었다!
레벨2의 첫 미션은 '리액트 맛보기'로서 기본개념을 습득하는 것을 목적으로 한다. 1, 2단계를 7일동안 소화해야해서 PR을 여느 때보다도 급한 마음으로 보내게 되었다. 레벨1에서 이미 경험해본 로또미션의 로직을 리액트로 옮겨오면서 리액트의 가상돔, JSX, 클래스 컴포넌트, 함수형 컴포넌트, 상태, 이벤트핸들링, Key, Ref, 생명주기 메서드 까지 얕게나마 접할 수 있었다.
미션 1단계 피드백 (1차)
👉 PR 바로가기
1. 컴포넌트 단위를 생각해보면
기존 로또 프로젝트를 옮겨오지 말고 리액트 관점에 맞춰서 재정의하는 것이 더 좋겠다. 컴포넌트는 무엇인지, 어떤 단위로 나누어야 하는지도 다시 고민해보자.
2. 배열의 key값
어떤 key를 넣어주어야 할지 고민이라면 리액트에서 리스트를 만들 때 고유한 key를 설정하도록 강제하는 이유를 먼저 생각해보자. (관련 코멘트)
3. props 스마트하게 전달받기
props를 spread해서 받으면 <Animation/> 컴포넌트에서 넣을 수 props가 추가 또는 변경되어도 유연하게 사용할 수 있다.
// 개선 전
export default class Animation extends Component {
render() {
const { height, speed, animationData } = this.props;
return (
<Lottie
height={height}
speed={speed}
options={{
animationData,
loop: false,
}}
/>
);
}
// 개선 후
export default class Animation extends Component {
render() {
const { animationData, loop, ...others } = this.props;
return (
<Lottie
options={{
animationData,
loop,
}}
{...others}
/>
);
}
4. 메서드는 더 깔끔하게
let 대신에 const 사용하자.
// 개선 전
getNumOfMatch(lotto) {
const { winningNumbers, bonusNumber } = this.props.drawNumber;
let numOfMatch = lotto.reduce((acc, cur) => acc + Number(winningNumbers.includes(cur)), 0);
if (numOfMatch === BONUS_CHECK_REQUIRED_COUNT && lotto.includes(bonusNumber)) {
numOfMatch += BONUS_COUNT;
}
return numOfMatch;
}
// 개선 후
getNumOfMatch(lotto) {
const { winningNumbers, bonusNumber } = this.props.drawNumber;
const numOfMatch = lotto.reduce((acc, cur) => acc + Number(winningNumbers.includes(cur)), 0);
if (numOfMatch === BONUS_CHECK_REQUIRED_COUNT && lotto.includes(bonusNumber)) {
return numOfMatch + BONUS_COUNT;
}
return numOfMatch;
}
reduce 활용해서 메서드를 정리하자.
// 개선 전
export const getMatchCount = (lottoBundle, winningNumber) => {
const matchCount = {};
lottoBundle.forEach((lotto) => {
const numOfMatch = getNumOfMatch(lotto, winningNumber);
matchCount[numOfMatch] = matchCount[numOfMatch] === undefined ? 1 : matchCount[numOfMatch] + 1;
});
return matchCount;
};
// 개선 후
export const getMatchCount = (lottoBundle, winningNumber) => {
return lottoBundle.reduce((acc, cur) => {
const numOfMatch = getNumOfMatch(cur, winningNumber);
acc[numOfMatch] = acc[numOfMatch] + 1 || 1;
return acc;
}, {});
5. 태그 선택에 유의
<section> 태그는 보통 헤딩과 함께 헤더, 푸터, 또는 메인을 구성하는 큰 구획에 사용한다. 단락을 구분한다는 의미라면 <p> 태그를 활용하는 것이 좋겠다.
6. 스타일링은 class로 제어
스타일링은 class로 제어한다 + BEM이라는 규칙으로 네이밍한다라는 것으로 연습해보면 좋겠다.
(참고: getbem, 에어비앤비 CSS 스타일가이드)
7. 더 적합한 네이밍으로
역할이 많이 섞여보인다. 애니메이션을 왜 실행시켜야 하는지를 기준으로 역할이 섞여보이지 않도록 네이밍을 해봐도 좋겠다. 또, getStatistics은 좀 일반적인 네이밍이라서 구체적인 네이밍을 고려해보면 좋겠다.
// 개선 전
export default class DrawNumbers extends Component {
constructor(props) {
...
this.state = {
rateOfReturn: 0,
shouldPlayAnimation: true,
};
}
componentDidMount() {
...
const result = getStatistics(lottoBundle, winningNumber);
...
}
}
// 개선 후
export default class DrawNumbers extends Component {
constructor(props) {
...
this.state = {
isLoading: true,
result: {
profit: 0,
rateOfReturn: 0,
},
componentDidMount() {
...
const result = getComputedResult(lottoBundle, winningNumber);
...
}
미션 1단계 피드백 (2차)
1. 높은 응집도 + 낮은 결합도
현재 컴포넌트로 만들어 놓은 <ResultSummary />는 재사용되지 않는다. 이는 단순 추출일 뿐이다. 의존성을 기반으로 추상화하고 재사용되는 부분을 컴포넌트로 만드는 연습이 필요하다. 예를 들어, <Record /> 컴포넌트를 만들면 재사용이 가능하다. '높은 응집도 + 느슨한 결합도'를 만족시키기 위해 계속해서 고민해보자.
*높은 응집도, 느슨한 결합도(High Cohesion, Low Coupling)는 1970년대 Larry Constantine과 Edward Yourdon이 정의한 소프트웨어 디자인 원리이다. (참고글: 응집도와 결합도 high cohesion loose coupling)
컴포넌트에 대한 이해도를 빠르게 높이는 첫 번째 방법은 잘 만들어진 오픈소스 디자인 시스템(e.g. 챠크라 UI)을 살펴보는 것이고, 두 번째 방법은 객체 지향의 책임과 역할을 공부하는 것이다. 두 번째 방법은 천천히 도전해 보면 좋겠다.
// 개선 전 - 재사용성 없는 컴포넌트
export default class ResultSummary extends Component {
render() {
const { profit, rateOfReturn } = this.props.result;
return (
<>
<p className="ResultSummary__profit">
<span className="ResultSummary__description">당첨 금액</span>
<span className="ResultSummary__value">{profit}원</span>
</p>
<p className="ResultSummary__rate_of_return">
<span className="ResultSummary__description">총 수익률</span>
<span className="ResultSummary__value">{rateOfReturn}%</span>
</p>
</>
);
}
}
// 개선 후 - 재사용성 있는 컴포넌트
export default class Record extends Component {
render() {
const { label, children } = this.props;
return (
<p className="Record">
<span className="Record__label">{label}</span>
<span className="Record__value">{children}</span>
</p>
);
}
}
2. 조건부 스타일 적용을 위한 라이브러리
class이름을 bind해주어 조건부 스타일을 깔끔하게 적용할 수 있는 'classnames'를 활용해보자.
// 개선 전
import './style.css';
export default class Text extends Component {
const { className, text } = this.props;
return <span className={`Text ${className}`}>{text}</span>;
}
// 개선 후
import classNames from 'classnames/bind';
import styles from './style.css';
const cx = classNames.bind(styles);
export default class Text extends Component {
const { className, text } = this.props;
const classnames = cx('Text', `${className}`);
return <span className={classnames}>{children}</span>;
}
3. props 스마트하게 전달하기
// 개선 전
<button type="button" className={`Button ${className}`} onClick={onClick}>
{text}
</button>
// 1차 개선 후
<button className={buttonClass} {...props}>
{children}
</button>
// 2차 개선 후
<button className={buttonClass} {...props} />
4. 내장메서드 padStart 활용
// 개선 전
import './style.css';
export default class Text extends Component {
const { className, text } = this.props;
return <span className={`Text ${className}`}>{text}</span>;
}
// 개선 후
import classNames from 'classnames/bind';
import styles from './style.css';
const cx = classNames.bind(styles);
export default class Text extends Component {
const { className, text } = this.props;
const classnames = cx('Text', `${className}`);
return <span className={classnames}>{children}</span>;
}
미션 2단계 피드백
👉 PR 바로가기
1. 컴포넌트 추상화 vs 단순 추출
추상화하지 않고 단순히 추출한 컴포넌트는 불필요한 props-drilling만 발생시킨다. 추상화를 하든, 단순 추출을 하든 분리 전에 분리가 정말 필요한 컴포넌트인지 먼저 생각해보자.
현재 <ResultTable> 컴포넌트의 경우, 이름은 도메인과 연관없지만 lottoBundle, winningNumber과 같이 도메인과 관련된 props를 받고 있다. 단순 추출을 할 것이라면 사용자의 로또 당첨결과를 렌더링한다는 역할 기준으로 네이밍을 하고, 범용적인 컴포넌트로서 추상화를 할 것이라면 다른 무언가의 결과가 렌더링될 필요가 있을 때도 사용할 수 있도록 합성가능한 컴포넌트로 작성해보자.
// 개선 전
export const ResultTable = (props) => {
...
return (
<div className="ResultTable">
<table className="ResultTable__table">
<thead>
<tr className="ResultTable__row">
<th className="ResultTable__head">구분</th>
<th className="ResultTable__head">번호</th>
</tr>
</thead>
<tbody>
{lottoBundle.map((v, i) => (
<ResultTableRow key={i} lotto={v} winningNumber={winningNumber} />
))}
</tbody>
</table>
</div>
);
};
// 개선 후
export const UserResultTable = (props) => {
...
return (
<Table>
<Thead>{theadItems}</Thead>
<Tbody>
{lottoBundle.map((lotto, index) => (
<TbodyRow key={index} rowIndex={index}>
{[getFirstCell(lotto), LottoBalls(lotto, index)]}
</TbodyRow>
))}
</Tbody>
</Table>
);
};
2. 컴포넌트 사용자의 입장에서 생각하기
컴포넌트 작성자 입장에서 벗어나 컴포넌트 사용자 입장에서 생각하면서, 좀 더 사용하기 편리한 컴포넌트를 만들어보자.
우선 컴포넌트 사용자가 props에 개별 style 속성을 하나하나 넣는 방식보다 className을 props로 받는 것이 외부에서 컴포넌트의 스타일을 커스텀해서 사용하기 편리하다.
또 컴포넌트의 주인공이 input 요소인 경우 props로 전달되는 input 요소의 attribute를 그대로 props에서 넘길 수 있도록 만들어야 해당 컴포넌트의 재사용성이 높아진다.
마지막으로, 컴포넌트의 기본값은 컴포넌트 사용자가 충분히 예상할 수 있는 값으로 작성해야 한다. 예를 들어 button의 type속성 기본값은 submit인데 이를 defaultProps에서 button으로 지정하고 있다면 이에 따른 혼동이 생길 수 있다
3. 명령형이 아닌 선언형으로
명령형(imperative)으로 작성하기보다 선언형(declarative)으로 작성해보자. 명령형/선언형 에 대한 개념 정립이 어렵다면 이 영상을 참고해보자.
onChange, reset 에 input disabled를 true/false toggle하는 것 => 명령형
input에서 다루고 있는 value 값에 따라 disabled가 제어되도록 하는 것 => 선언형
4. Controlled vs Uncontrolled
Input 컴포넌트를 만들 때에는 디자인 단계에서 제어 컴포넌트로 만들 것인지 비제어 컴포넌트로 만들 것인지 먼저 고민해보자. 비제어 컴포넌트로 사용하려고 했다면 ref를 받을 수 있도록 해당 함수 컴포넌트를 forwardRef로 감싸서 export 해주고, Controlled 컴포넌트 사용하려고 했다면 value 값(ToggleButton에서는 checked)을 받을 수 있도록 props에 정의해주어야 한다.
5. 커스텀훅 작성 연습하기
커스텀훅을 만드는 연습을 시작해보자. react-use, react-spectrum 과 같이 잘 만들어진 Hooks 오픈소스도 살펴보자. hooks가 컴포넌트를 내포하게 되면 UI와 의존성이 생기게 되겨 재사용성이나 확장성에 불리해진다.
export function useToggle(initialValue = false) {
return useReducer(prev => !prev, initialValue);
}
6. 기타
PropTypes.any는 아무런 검사 효과가 없다. PropTypes.element 타입을 활용해보자.
Boolean으로 형변환을 하면 falsy 값일 경우 오류가 발생할 수 있다. Boolean이 아닌 값을 캐스팅해서 사용하기 보다는 상황에 맞는 표현식을 써주는 것이 좋겠다. '!array.length' 보다 'array.length === 0' 가 더욱 명시적이다.
다른 크루의 리뷰 메모 👀
피드백 내용 앞의 번호는 순번이 아니라 PR 번호이다. 지금은 아예 이해가 안되는 내용도 있지만... 서당개 3년이면 풍월을 읊는다 했다!
리액트 일반
#02 이터러블의 key값으로는 '고유한 문자열'을 넣는 것이 좋다. 객체 선언부에서, uuid 등 id 생성기를 사용하여 고유한 id를 만들어 key로 사용하는 방법을 많이 사용한다. 이번 미션에 id 생성기까지 필요하지는 않다고 생각된다면, winning-number-${key} 형태로 고유한 이름을 정해주는 방법이 있다.
#06 state를 인자로 받아서 순수함수로 만들어보는 것도 좋다.
#07 취향의 영역이긴 하지만 state나 props는 구조 분해 할당해서 사용하는 것이 깔끔하다.
#10 setState할 때 꼭 모든 프로퍼티를 넘겨줄 필요 없이, 변경하고자 하는 프로퍼티의 key, value만 명시해주면 된다. (참고: 'State Updates are Merged')
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts // *** posts만 변경, comments 영향 x
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments // *** comments만 변경, post 영향 x
});
});
}
#11 ref.current에 접근해서 DOM을 직접 수정한다면, 리액트에서 제공하는 Lifecycle 혹은 Virtual DOM 렌더링 뎁스가 꼬일 위험이 매우 높아진다. ref를 여러 군데서 호출하고 있다면 어디에서 로직이 수정되었는지 추적이 더욱 어려워진다. current가 undefined인 경우도 고려해야한다. 이렇게 side-effect가 존재하는 ref의 사용보다는 가능하다면 리액트의 Lifecycle을 따르는 것이 좋다.
#11 setState를 자식으로 내려보내줄 때 어떤 곳에서 어떻게 쓰이는지에 대해 더 구체적이고 명확한 이름을 붙여서 내려보내주면 추적이 용이해져 디버깅에 도움이 된다.
#13 리액트에서 innerText와 같이 DOM을 직접적으로 조작하는 코드는 지양해야 한다. 같은 맥락에서 state로 충분히 처리할 수 있는 경우에까지 ref를 남용해서는 안된다.
#14 리스트를 렌더링할 때 1씩 증가하는 id를 반환하는 함수를 사용하기도 한다. 아래와 같이 작성하면 된다.
const generateId = (() => {
let id = 0;
return () => ++id;
)()
#24 eslint-plugin-react 의 jsx-handler-names 룰은 컴포넌트에서 자체적인 핸들러를 정의할 때는 handle prefix를, 부모 컴포넌트로부터 핸들러를 props로 전달받을 때에는 on prefix 로 핸들러를 전달받도록 강제한다. (링크)
#25 App이 비즈니스로직으로 비대해진다면 custom hook을 사용해서 비즈니스 로직과 상태를 분리해내는 방법을 사용해보자.
#25 단순히 index가 증가하는 id보다 좀 더 유니크한 id를 만들어줄 수 있도록 다음의 방법을 사용할 수 있다.
const instance = Math.round(Math.random() * 1000000000)
let index = 0
// 사용처에서
`${instance}-${index++}`
#43 핸들러는 정보를 가장 많이 알고 있는 컴포넌트(aka 정보전문가)에서 정의되는 것이 좋습니다.
#46 useMemo, useCallback은 함수 또는 객체의 참조를 유지하고자 하는 필요가 있을 경우, 성능에 문제가 있다는 것이 인지된 경우에 사용한다.
#46 useState 대신 다음과 같이 useReducer를 사용할 수 있다.
// useState 사용 시
const [isToggleOn, setToggle] = useState(false);
// useReducer 사용 시
const [isOn, toggle] = useReducer(prev => !prev, false);
#56 렌더링 관련 성능 문제가 생기기 전에 useCallback를 굳이 사용할 필요는 없다.
컴포넌트 설계
#02 리액트에서 컴포넌트는 뷰 컴포넌트를 뜻한다. 따라서 Display라는 네이밍은 Redundant하다.
#04 class의 장점은 객체 단위로 표현할 수 있어, 공통 부분을 추상화해서 상속을 통해 코드량을 줄이되, 구조를 명확하게 구현할 수 있다는 점이다. 예를 들어 <Form />을 부모 컴포넌트로 만들고, 자식 클래스로 <~Form /> 을 구현해서 <Button />, <Input /> 컴포넌트나, onSubmit 이벤트에 대해 Form Validation에 대해서도 추상화를 통해 절대적인 로직을 줄일 수 있다. 이는 함수 컴포넌트에서 할 수 없는 클래스 컴포넌트만의 장점이다.
#05 '확인' 버튼과 '결과 확인하기' 버튼은 재사용 가능한 하나의 버튼 컴포넌트로 만드는 것이 좋겠다.
#05 <Modal /> 이라는 이름의 컴포넌트가 당첨 결과를 직접 알고 있을 경우, 전혀 다른 화면이 필요한 모달 컴포넌트는 네이밍부터 애매해진다. 당첨 결과를 직접 알고 있다면 <Modal /> 보다는 <LottoResultModal />과 같이 더 구체적인 이름으로 변경하거나 <Modal /> 역할만 하고 당첨 결과는 외부에서 받도록 해야 한다. 당첨 결과가 지금은 모달로 뜨지만 실무에서는 페이지 이동해서 뜨게 해달라는 식으로 요구사항 변경이 얼마든지 있다. 이런 상황을 염두에 두고 재사용 가능한 컴포넌트를 설계하는 것이 좋다.
#09 입문자들은 특히 모달과 같은 컴포넌트를 재사용하지 못하게 만들곤 한다. 모달의 컨텐츠를 children으로 받는다면 재사용 가능하게 만들 수 있다.
#10 컴포넌트 분리에 대한 고민은 함수를 어디서 어떻게 분리할 지에 대한 고민과 매우 흡사하다. 때로는 추상화하지 않고 중복을 허용하는 것이 더 좋을 때도 있다. 컴포넌트 분리에 절대적인 기준이 없어 경험적으로 습득해야 한다.
#15 컴포넌트 파일 내에 존재하지만 컴포넌트 외에 존재하는 것이 나은 함수도 있다. 꼭 해당 함수가 컴포넌트 안에 있어야 하는지 고민해보자. 이를 구분하기 위해 순수함수와 프로시저의 개념을 살펴보는 것도 좋겠다. (참고)
- 순수함수: 실행시점이 변경되어도 동일한 반환값을 반환한다. 외부변수에 접근하지 않는다.
- 프로시저: 실행시점에 따라 다른 효과를 가진다. 외부 변수를 사용 또는 변경한다. (비순수함수이다.)
#19 리액트 상에서 보여질지를 결정하는 변수라면 isModalOpen 보다 isVisibleModal 이 더 좋은 네이밍이다.
#43 Modal 컴포넌트가 책임져야만 하는 스타일 까지만 정의해 두고 그 외 스타일은 전부 override 할 수 있도록 열어보자.
#43 실제로 프로덕트를 만들다 보면 아토믹 패턴의 atom, molcules, organism 이렇게 세 단계로 딱 나누어 떨어지지 않는다. 이미 아토믹 디자인을 적용해서 지켜야한다는 이유로 구조가 망가지고 제어가 역전되는 상황이 발생한다. 실제로 적용해보신 분들은 molcules, organism 이 두 가지에 대한 구분이 사람마다 달라 혼란을 겪었다고 말씀하신다.
상태관리/이벤트핸들링
#02 this.setState를 굉장히 빠른 시간내에 반복하지 않는이상 setState는 비교적 안전하다. 이전 state로 새 state를 만드는 첫 번째 예시에는, 'this.state.count + 1' 가 다르게 동작할 수 있으므로 함수를 전달하는 것이 적절하다. 이전 state와 상관없이 새 state를 만드는 두 번째 예시에서는 객체를 전달하는 것이 적절하다.
// 이전 state로 새 state를 만드는 경우
this.setState({count: this.state.count + 1}); // X
this.setState((state) => { // O
return {count: state.count + 1}
});
// 이전 state와 상관없이 새 state를 만드는 경우
this.setState({isValid: false}); // O
#02 프로젝트 구조가 복잡하지 않다면 Context API와 같은 이벤트 버스를 만들어서 이벤트나 상태를 관리하지 않고 단순히 Prop-Drilling(*)만으로도 충분히 구현할 수 있다. (Kent C. Dodds의 Prop-Drilling 참고자료) Context API를 사용하면 props 등의 얕은 복사가 안되서 상태가 하나라도 변경되면 모든 것들이 다시 렌더될 수 있다.
#02 Consumer는 함수 컴포넌트에서 Context API의 변화를 감지하기 위해 사용한다.
#03 리액트는 내부적으로 document 노드에(리액트 17버전 이후로는 root 요소에)에 이벤트를 위임하고 최적화하고 있어 특수한 경우가 아니라면 성능적으로 신경쓰지 않아도 된다.
#03 상태를 상위 컴포넌트로 끌어올리는 방식 상태 끌어올리기(State Lifting, 공식문서)은 자주 사용되는 방법이다.
#05 굳이 비제어 컴포넌트를 사용해야 할 이유가 없다면 리액트 개발진이 권장하는 방법인 제어 컴포넌트를 사용하자. 제어 컴포넌트를 사용하면, 코드도 간결해지고 다양한 상황에 더 쉽게 대응할 수 있게된다. (p.s. 5년간 리액트를 사용하면서 비제어 컴포넌트를 사용해야할 경우는 없었다.)
#07 <input>에서 지정한 name을 이용해서 <input>의 value를 업데이트해줄 수 있다. 이 방법은 특히 하나의 <form> 내부에 여러 개의 input이 있을 때 유용하다. (참고: input 상태 관리하기)
// <input />의 onChange 핸들러
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
#12 부모 밑의 자식 트리가 100단계까지 있는 경우를 가정해보자. 부모의 state 하나를 업데이트 하기위해서는 버블링으로 함수가 100번실행될 수도 있다. 이러한 측면을 고려해보면 상태관리 라이브러리나 Context API 의 필요성을 자연스럽게 느낄 수 있다. 느끼게 될거에요!
#15 setState API의 Syntax는 다음과 같다. (참고: 리액트 공식문서)
setState(updater, [callback])
예외처리
#04 실제 서비스를 기준으로 안전하게 코딩하기 위해서는 배열과 파라미터에 대한 꼼꼼한 예외처리가 필요하다. 그렇지 않으면 심한 경우 앱이 오동작 할 수 있다. webpack이 똑똑해졌긴 하지만 가끔 갱신이 안되기도 하듯 웹 환경은 우리의 생각보다 안전하지 않다! 예외처리 로직이 어렵다면, 최소한 컴포넌트 단위에서 try...catch를 사용해 값을 빈 배열 혹은 초기값을 필수적으로 넣어줄 수 있어야 한다.
onSubmitPrice(event) {
event.preventDefault();
try {
const price = event.target.price.value || 0;
...
} catch (e) {
alert(e.message);
}
}
#11 에러처리는 프로덕션에 나가는 코드 기준으로 생각해야 한다. 예를 들어, 고객이 실제로 사용해야하는 Form과 같은 부분에서 런타임 에러가 발생하면 정말 큰 문제이다. 고객이 에러를 만나지 않도록 하는 것이 가장 중요한 첫 번째이고, 코드를 읽는 사람이 에러 추적을 용이하게 할 수 있는 것은 그 다음의 문제이다.
#16 submit 이벤트는 중요한 이벤트이고, 때에 따라서는 API 요청도 필요로 한다. 따라서 try...catch로 감싸 예외처리 추가하는 것이 좋겠다.
함수/메서드
#09 Array.from()은 유사배열 객체 또는 이터러블을 얕은 복사하여 Array 객체를 만드는 메서드이다. Array.from의 두 번째 인자 mapFn는 Array.from().map()과 같은 형태로 동작하므로, 리턴 값을 사용하는 경우에 활용할 수 있다.
Array.from([1, 2, 3], x => x + x);
// [ 2, 4, 6 ]
Array.from({ length: 4 }, () =>
String.fromCodePoint(129300 + Math.floor(Math.random() * 30))
)
// [ '🤩', '🤚', '🤖', '🤚' ]
#10 constructor에서 별다른 프로퍼티를 설정하지 않는다면 생략할 수 있다.
// 생략 가능
constructor() {
super();
}
// 생략 가능
constructor(props) {
super(props);
}
// 생략 불가
constructor(props) {
super(props);
this.state = ...
}
#12 객체의 프로퍼티 키와 변수 이름이 같다면 프로퍼티 축약 표현을 이용해서 더 간결하게 표현할 수 있다.
// 프로퍼티 축약 전
this.setState({ winningCounts: winningCounts });
this.setState({ winningCounts });
HTML/CSS
#02 style 코드, 레이아웃 코드, 비즈니스 코드를 적절히 분리하는 것이 좋겠다. js파일에서 CSS를 inline으로 넣는 것보다 .css 파일을 분리해서, 각 파일을 띄워놓고 양 옆으로 보면서 코딩하는게 생산성에 더 좋다. 수정이 필요할 때도 HTML 태그를 뒤져보지 않고도 class의 이름만으로 해당 스타일을 바로 수정할 수 있다.
#04 CSS Modules를 적용하면 CSS 구조를 더 편하게 잡을 수 있고, class 네이밍에서 prefix를 생략할 수 있게 된다.
#11 반응형 대응 코드는 변수로 따로 관리하는 것이 좋다.
@media screen and (max-width: 768px) {
...
}
#15 HTML 태그가 가지고 있는 기본 스타일을 사용하고 싶지 않은 경우, div 태그를 이용하기보다는 태그는 그대로 semantic하게 사용하고 스타일 이슈는 CSS로 해결하는 것이 좋다. 예를 들어 svg로 'X' 모양을 살려 버튼을 구현하고 싶다면 태그는 <button>으로 하되, 기능상 버튼인데 <button>이 가지고 있는 기본 스타일을 CSS로 제어하는 것이 좋겠다.
#43 IE에는 main 태그가 없다 (...)
환경설정
#02 'eslint-config-airbnb-base' 는 VanillaJS 규칙만 있고 리액트 룰까지 적용하려면 'eslint-config-airbnb' 를 설정하는 것이 좋다.
그 외 키워드: (#02) 이벤트 버스, Prop-Drilling, (#19) 타입스크립트의 type vs interface
'Clean Code' 카테고리의 다른 글
[PR 코드리뷰] 우아한테크코스 프론트엔드 레벨2 - 리액트 장바구니 미션 (0) | 2021.05.17 |
---|---|
[PR 코드리뷰] 우아한테크코스 프론트엔드 레벨2 - 리액트 페이먼츠 미션 (0) | 2021.05.03 |
[수업 중 코드리뷰] 우아한테크코스 프론트엔드 레벨1 총정리 (2) | 2021.04.10 |
[PR 코드리뷰] 우아한테크코스 프론트엔드 레벨1 - 지하철 노선도 미션 (2) | 2021.03.23 |
[PR 코드리뷰] 우아한테크코스 프론트엔드 레벨1 - 나만의 유튜브 강의실 미션 (1) | 2021.03.07 |