React 공식문서 톺아보기 (feat.코어 자바스크립트)
리액트 개발자들은 공부를 시작한 순간, 심화 기술에 대한 레퍼런스 참고 시, 새로운 버전이 출시되면 어김없이 공식문서를 찾게 된다.
수 많은 테크 블로그들의 정보와 비교해 가장 심플하고 가장 직관적이며 가장 정확한 정보를 가지고 있기에 뗄레야 뗄 수 없다.
필자는 최근 한 동안 코어 자바스크립트 공부에 빠져 실행 컨텍스트, 스코프, 비동기에 대한 기본기를 다졌다. (이에 대한 내용은 느낀 바가 많아 정리 후 포스팅 할 예정이다) 자바스크립트 엔진이 동작하는 방식을 이해하고 다시 읽는 공식문서는 좀 색달랐다.
예전에는 그저 페이스북이 정한 법칙대로. 로마에 왔으면 로마의 법을 따르라는 것 마냥 "그냥 그렇게 써야 돼"로 문서를 받아들였다면, 지금은 "로컬 state를 적용하기 위해서 왜 function 컴포넌트가 아닌 class 컴포넌트를 써야할까?"가 궁금해졌다.
클래스형 컴포넌트는 자유롭게 state에 접근할 수 있지만, 함수형 컴포넌트는 state를 직접 사용하진 못하고 다른 메소드를 통해서 사용
클래스형 컴포넌트 :
state기능 및 라이프사이클 기능 사용 가능
임의 메서드 정의 가능
render 함수 정의 필수
jsx 반환 필수
함수형 컴포넌트 :
선언하기 간편
메모리도 클래스형 컴포넌트보다 덜 사용
빌드 후 배포할 때 파일 크기가 더 작음 (사실상 별 차이 없음)
state와 라이프사이클 API의 사용이 불가능
-> 리액트 v16.8업데이트 후 Hooks 도입 후 해결
(완전히 클래스형 컴포넌트와 같지는 않지만 조금 다른 방식으로 비슷한 작업 가능)
(리액트 공식 권장: 함수형 컴포넌트 + Hook )
결론 :
리액트 공식 문서에서는 가능하면 함수형 컴포넌트와 React Hooks를 사용할 것을 권장하고있다. 따라서 함수형 컴포넌트와 함께 useState를 사용하자!
(결국 리액트도 자바스크립트로 만들어진 라이브러리이기 때문에 기본적으로 vanillaJS에 대한 이해가 있다면, 모든 것이 자연스럽게 받아들여질 수 있다!)
다시 읽어보는 공식문서는 궁금증과 나름대로의 해답을 내려가는 과정이 흥미로웠고, 이 과정을 기억하고자 포스팅 해 본다 ✍🏻
React 최상위 API 불러오기
//ES6
import React from 'react’
//ES5
var React = require('react')
React 엘리먼트
불변객체
생성 이후에는 자식이나 속성 변경 불가
UI를 업데이트하는 유일한 방법 → 새 엘리먼트를 생성하고 ReactDOM.render()로 전달
React 컴포넌트
UI를 독립적이고 재사용할 수 있는 부분으로 분리
- React.Component
class Greeting extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
- ES6 class 사용
- React.PureComponent컴포넌트에 대하여 얕은 비교만 수행해 성능 향상 가능자식 컴포넌트들이 “순수”한지 확인 필요
- props와 state의 구조가 간단할 것으로 예상될 때에만 사용
- shouldComponentUpdate() 구현 X
React.memo()
const MyComponent = React.memo(function MyComponent(props) {
/* props를 사용하여 렌더링 */
});
고차 컴포넌트
컴포넌트가 동일한 props로 동일한 결과를 렌더링 시 memo 호출
결과를 메모이징(Memoizing)하도록 래핑
컴포넌트를 렌더링하지 않고 마지막 렌더링 결과를 재사용
→ 불필요 렌더링 줄여 성능 최적화
props변화에만 영향을 줌
props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행
→ 래핑 함수컴포넌트에 useState, useReducer, useContext hook을 사용하면 여전히 state, context 변화 시 재렌더링
→ 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하
현재 state 또는 props의 변화 시 true를 반환하는 shouldComponentUpdate() 메서드와 반대!
*/
}
export default React.memo(MyComponent, areEqual);
React.createElement()
React.createElement(type, [props],[...children])
인자로 주어지는 타입에 따라 새로운 React 엘리먼트를 생성 반환
인자로 JSX 사용 가능
보통 React 컴포넌트를 만들 땐 createElement(), createFactory() 대신 JSX를 사용할 것을 권장!
render()
ReactDOM.render(element, container[, callback])
React 엘리먼트를 container DOM에 렌더링
컴포넌트에 대한 참조를 반환 (무상태 컴포넌트는 null을 반환)
container 하위에 렌더링 이력이 있다면 업데이트
변경된 React 엘리먼트 DOM만 변경
추가 콜백 제공 시 컴포넌트가 렌더링되거나 업데이트된 후 실행
🚀 참고
- 처음 호출할 때 기존의 DOM 엘리먼트를 교체하며 이후의 호출은 React의 DOM diffing 알고리즘을 사용
- 컨테이너 노드를 수정하지 않고 컨테이너의 하위 노드만 수정
- 자식 노드를 덮어쓰지 않고 기존 DOM 노드에 컴포넌트를 추가 가능
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
// root 객체에 render하는 다른 방법
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(element);
// React.createElement(type, [props],[...children])
// 명세 : 인자로 주어지는 타입에 따라 새로운 React 엘리먼트를 생성하여 반환
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
React.Children
불투명(opaque) 자료구조 this.props.children을 다루는 유틸리티 함수들을 제공
React.Children.toArray
각 자식에 key가 할당된 배열을 불투명 자료구조로 반환
render()메서드에서 children의 집합을 다루고 싶을 때
this.props.children을 하부로 전달하기 전 재정렬하거나 일부만 잘라내고 싶을 때에 유용
React.Fragment
render() 메서드 내에서 추가 DOM 엘리먼트를 생성하지 않아도 여러 엘리먼트를 반환
축약형인 <></>문법으로도 동일
render() {
return (
<React.Fragment>
Some text.
<h2>A heading</h2>
</React.Fragment>
);
}
React.Suspense
트리 상 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때
로딩 지시자를 나타낼 수 있음
React.lazy와 Suspense는 아직 서버 사이드 렌더링 불가
서버에서 렌더링 된 앱에서 코드 분할을 하기 원한다면 Loadable Components 추천
// 이 컴포넌트는 동적으로 불러옵니다
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
// Displays <Spinner> until OtherComponent loads
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}
Hooks
class를 사용하지 않아도 state와 React 기능들을 사용
주요개념>4.Component와 Props
React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견
JSX attributes와 child를 해당 컴포넌트에 단일 객체로 전달 → 이 객체가 “props”!
props의 이름은 사용될 context가 아닌 컴포넌트 자체의 관점에서 짓는 것을 권장
props는 readonly!
→ 순수함수 : 입력값을 바꾸려 하지 않고 항상 동일한 입력값에 대해 동일한 결과를 반환
// 순수함수 O
function sum(a, b) {
return a + b;
}
// 순수함수 X
function withdraw(account, amount) {
account.total -= amount;
}
🚀 모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 합니다.
→ 순수함수 규칙을 위반하지 않기 위해 state를 사용!
(사용자 액션, 네트워크 응답 및 다른 요소에 대한 응답으로 시간에 따라 자신의 출력값을 변경)
주요개념>5.State와 생명주기
Clock컴포넌트에 setInterval()로 타이머를 설정
매초 UI를 업데이트하는 것을 외부가 아닌 컴포넌트Clock의 내부로 옮기고 싶을 때
이상적으로 한 번만 코드를 작성하고 Clock이 스스로 업데이트하도록 만들려면?
→ State 사용!!!
Clock 컴포넌트코드 상세 정리
const root = ReactDOM.createRoot(document.getElementById('root'));
// function Clock(props) {
// return (
// <div>
// <h1>Hello, world!</h1>
// <h2>It is {props.date.toLocaleTimeString()}.</h2>
// </div>
// );
// }
// function tick() {
// root.render(<Clock date={new Date()} />);
// }
// setInterval(tick, 1000);
// function 컴포넌트에 state 적용하기
// 1. 함수에서 클래스로 변환하기
// 2. 클래스에 로컬 State 추가하기
// 3. 생명주기 메서드를 클래스에 추가하기 => 마운팅 / 언마운팅
// 4. 컴포넌트 로컬 state 업데이트 => tick() 메서드 구현
class Clock extends React.Component {
// 2. this.state를 지정하는 constructor 추가
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
// 3. 컴포넌트 출력물이 DOM에 렌더링 된 후에 실행
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
// 4.로컬 state 업데이트하기
tick() {
this.setState({date: new Date()});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
{/*
1. render() 내용 안에 있는 props를 this.props로 변경
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
2. this.state로 변경
*/}
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock/>,
document.getElementById("root")
);
setState()에 대한 3가지
- 직접 State 수정하지 말 것
- this.state를 지정하는 유일한 공간은 contstructor
- this.state를 불변적(Immutable)인 데이터로 취급해라! → setState() 사용
- State 업데이트는 비동기적일 수 있음
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct 1 - 화살표 함수 형태
// 객체보다는 콜백함수를 인자로 사용!!
// state : 이전 state, props : 변경할 값
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
// Correct 2 - 일반 함수 형태
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
- State 업데이트는 병합된다.
- 별도의 호출로 변수를 독립적으로 업데이트
- 병합은 얕게 이루어짐
- this.setState({comments})는 this.state.posts에 영향을 주진 않지만 this.state.comments는 완전히 대체
componentDidMount() {
fetchPosts().then(
response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(
response => {
this.setState({
comments: response.comments
});
});
}
- 병합은 얕게 이루어짐
- 제공한 객체를 현재 state로 병합
State
props와 유사
비공개이며 컴포넌트에 의해 완전히 제어
데이터는 아래로 흐른다
state = 로컬 = 캡슐화
컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있음
“하향식(top-down)” 또는 “단방향식” 데이터 흐름
<FormattedDate date={this.state.date} />
/*
FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고
이것이 Clock의 state로부터 왔는지,
Clock의 props에서 왔는지,
수동으로 입력한 것인지 알지 못합니다.
*/
API참고서>React>React.Component
마운트
컴포넌트의 인스턴스가 생성되어 DOM 상에 삽입될 때 순서대로 호출
- constructor() : 메서드를 바인딩하거나 state를 초기화하는 작업이 없다면, 생략 가능
- static getDerivedStateFromProps() : render메서드를 호출하기 직전에 호출
- render() : 클래스 컴포넌트에서 반드시 구현돼야하는 유일한 메서드
- componentDidMount() : 컴포넌트가 마운트된 직후, 즉 트리에 삽입된 직후
업데이트
props 또는 state가 변경되면 발생
아래 메서드들은 컴포넌트가 다시 렌더링될 때 순서대로 호출
- static getDerivedStateFromProps()
- shouldComponentUpdate() : 현재 state 또는 props의 변화가 컴포넌트의 출력 결과에 영향을 미치는지 여부를 확인. 기본 동작은 매 state 변화마다 다시 렌더링을 수행하는 것
- render()
- getSnapshotBeforeUpdate() : 가장 마지막으로 렌더링된 결과가 DOM 등에 반영되기 전에 호출. 이 생명주기 메서드가 반환하는 값은 componentDidUpdate()에 인자로 전달
- componentDidUpdate() : 갱신이 일어난 직후에 호출. 최초 렌더링에서는 호출되지 않음
마운트 해제
• componentWillUnmount()
오류 처리
- static getDerivedStateFromError()
- componentDidCatch()
class 프로퍼티
- defaultProps : null이 아닌 아직 정의되지 않은 undefined 인 props를 다룰 때 사용
- displayName : 디버깅 메시지 표시에 사용
Ref
[React] 클래스형 state vs 함수형 state