일.Class Component
Class 는 의심할 여지 없이 가장 널리 사용되는 React 컴포넌트 형식입니다. 예를 들어:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
가장 완전한 라이프사이클 지원을 가지며,新旧합쳐 총 13 개가 있습니다:
// Mounting
constructor()
render()
UNSAFE_componentWillMount()
componentDidMount()
// Updating
static getDerivedStateFromProps()
shouldComponentUpdate()
getSnapshotBeforeUpdate()
UNSAFE_componentWillReceiveProps()
UNSAFE_componentWillUpdate()
componentDidUpdate()
// Unmounting
componentWillUnmount()
// Error Handling
static getDerivedStateFromError()
componentDidCatch()
게다가, State, Props, Context, Ref 등의 특성도 있습니다. 이러한 서포트로 인해, Class 는완전한 컴포넌트 특성을 가진 유일한 선택지 가 되었습니다. Class 에도 많은 문제가 존재함 에도 불구하고, 그것은 대체 불가능합니다
P.S. 각 라이프사이클의 의미와 그 작용에 대해서는, React | 黯羽輕揚 참조
이.Function Component
또 다른 컴포넌트 형식은 함수로, Props 를 입력하고, React Element 를 출력합니다. 예를 들어:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
가장 간단한 React 컴포넌트 형식으로:
The simplest way to define a component is to write a JavaScript function.
라이프사이클조차持たないほど 간단 하며, State 는 말할 것도 없습니다. 이러한 제한으로 인해, 함수형 컴포넌트는 매우 간단한 View Component 로만 사용될 수 있으며, 중임을 맡을 수 없습니다. 상당히 긴 시간 동안, "교육" 용도로만 사용되었습니다:
Classes have some additional features that we will discuss in the next sections. Until then, we will use function components for their conciseness.
React 16 이후, 함수형 컴포넌트가 단계적으로 강화되었습니다:
-
createRef/forwardRef: React 16.3 이후, 함수형 컴포넌트가 Ref 를 서포트
-
[React.memo](/articles/react-16-6 新 api/#articleHeader2): React 16.6 이후, 함수형 컴포넌트도 "shouldComponentUpdate"를 맞이했습니다
물론, 가장 중요한 강화는 자연스럽게 Hooks 입니다:
Hooks 를 통해, 함수형 컴포넌트도 State, 라이프사이클 등의 Class 컴포넌트 특성 (state, lifecycle, context, ref 등) 을 가질 수 있습니다
P.S. Hooks 에 대한 상세 정보는, React Hooks 简介 참조
삼.Function Component with Hooks
간단히 말해, Hooks 가 생긴 후, 함수형 컴포넌트는 Class 컴포넌트와 거의 동일한 표현력을 갖게 됩니다. 다양한 라이프사이클, State 등을 포함
If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.
예를 들어:
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = React.useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState Hook 을 통해, 함수형 컴포넌트가 State 를 갖게 되었습니다. 마찬가지로, 라이프사이클, Context, Ref, 컴포넌트 인스턴스 속성 등의 특성도 유사한 Hook 방식으로 서포트가 제공됩니다. 상세는 다음과 같습니다:
| Hook | 특성 | Class 와의 비교 |
|---|---|---|
| useState | State | this.statethis.setState(newState) |
| useEffect | 라이프사이클 | componentDidMountcomponentDidUpdate |
| useContext | Context | this.context |
| useReducer | State | Redux Reducer 식의 State 관리 |
| useCallback | Function Props | this.myMethod.bind(this) |
| useMemo | 퍼포먼스 최적화 | 중복 계산 회피 |
| useRef | Ref | createRef |
| useImperativeHandle | 컴포넌트 인스턴스 속성/메서드 | forwardRef |
| useLayoutEffect | 라이프사이클 | 동기componentDidMount동기 componentDidUpdate |
| useDebugValue | 디버그 | Hooks 상태의 가시화 (React DevTools 에서this.state를 보는 것과 유사) |
사.Migrate Class to Hooks
물론, 기존 Class 컴포넌트를 Hooks 로 리팩토링할 필요도 없고,也不可能합니다:
There is no rush to migrate to Hooks. We recommend avoiding any "big rewrites", especially for existing, complex class components.
We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future. At Facebook, we have tens of thousands of components written as classes, and we have absolutely no plans to rewrite them.
여기서는 Hooks 와 Class 특성의 대응 관계에 대해서만 서술할 뿐이며, 이 유비는 Hooks 의 이해에 도움이 됩니다
constructor()
생성자에서 가장 중요한 조작은 this.state 의 선언/초기화로, State Hook 을 통해 완료합니다:
class Example extends React.Component {
constructor() {
this.state = {
count: 0
};
}
}
다음과 동등:
function Example() {
// 초기값 0 의 state 변수를 선언
const [count, setCount] = React.useState(0);
}
그 구문 형식은:
const [state, setState] = useState(initialState);
그 중에서 const [state, setState] = xxx 는 구조 분해 할당 구문입니다 (상세는 [destructuring(구조 분해 할당)_ES6 노트 5](/articles/destructuring(구조 분해 할당)-es6 노트 5/) 참조). 다음과 동등:
const stateVariable = useState(initialState); // 페어를 반환
const state = stateVariable[0]; // 페어의 첫 번째 항목
const setState = stateVariable[1]; // 페어의 두 번째 항목
상태값 (state) 과 대응하는 Setter(setState) 를 반환합니다. Setter 를 호출하면 컴포넌트의 업데이트가 트리거됩니다 (Class 내의 this.setState 와 유사)
초기값 initialState 는 첫 렌더링에만 작용하며 (반환값 state 를 통해 취득), 이후 state 는 업데이트를 유지합니다
特殊的, 여러 개의 상태 변수가 필요한 경우, useState 를 여러 번 호출합니다:
function ExampleWithManyStates() {
// 여러 개의 상태 변수를 선언!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
물론, 상태값은 오브젝트 또는 배열也可以是지만, this.setState() 처럼 merge 는 수행되지 않습니다:
Unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
예를 들어:
function ExampleWithManyStates() {
const [profile, setProfile] = useState({
age: 42,
favorite: 'banana',
todos: [{ text: 'Learn Hooks' }]
});
setProfile({
todos: []
});
// Class 중의 다음과 동등
this.setState({
age: undefined,
favorite: undefined,
todos: []
});
// 가 아니라
this.setState({
todos: []
});
}
render()
함수형 컴포넌트 자체가 render() 함수로, Props, State 등의 데이터를 뷰에 주입하고, 이벤트 처리 로직을 등록합니다:
class Example extends React.Component {
/* state 초기화 부분을 생략 */
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
다음과 동등:
function Example() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Class 컴포넌트와 달리, 함수형 컴포넌트의 State 값은 State Hook 을 통해 취득합니다 (상예 중의 count). this.state 가 아닙니다.相应的, this.setState() 도 useState() 가 반환하는 Setter 를 통해 완료합니다
UNSAFE_componentWillMount()
첫 렌더링 시 render() 전에 트리거되며, constructor() 와 기능이 조금 중복되어 있습니다.前述의 constructor() 부분을 참조하세요
componentDidMount()
componentDidMount 에는 일반적으로 부수 효과를 동반하는 조작이 포함됩니다. 함수형 컴포넌트에서는 Effect Hook 으로 대체할 수 있습니다:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
/* render 부분을 생략 */
}
다음과 동등:
function Example() {
const [count, setCount] = useState(0);
// componentDidMount 와 componentDidUpdate 에 유사:
useEffect(() => {
// 브라우저 API 를 사용하여 문서 제목을 업데이트
document.title = `You clicked ${count} times`;
});
/* return 부분을 생략 */
}
Effect Hook 은 컴포넌트의每次렌더링 종료 시 트리거되므로, Class 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount 에 상당합니다
구문 형식은:
useEffect(didUpdate);
컴포넌트가每次 (첫 회 포함) 렌더링 후에 무언가를 수행할 필요가 있음을 나타냅니다:
The function passed to useEffect will run after the render is committed to the screen.
mounting 과 updating(componentDidMount 과 componentDidUpdate) 을 구분할 필요가 있는 경우, 의존 관계를 선언함으로써 완료할 수 있습니다. 상세는 Tip: Optimizing Performance by Skipping Effects 참조
실행/클린업을 1 회만 수행할 필요가 있는 부수 효과의 경우, 그것이 컴포넌트의 상태에 의존하지 않음을 선언합니다 (useEffect(didUpdate, [])).此時 componentDidMount 에 componentWillUnmount 를 더한 것과 동등해집니다
그러나, Fiber 스케줄 메커니즘 으로 인해, Effect Hook 은 동기 트리거가 아닙니다. 따라서 DOM 상태를 읽을 필요가 있는 경우, 동기의 LayoutEffect Hook 을 사용합니다
P.S. 따라서, 엄밀히 말하면, LayoutEffect Hook 이 componentDidMount, componentDidUpdate 등의 라이프사이클과 동등한 Hooks API 입니다. 그러나 퍼포먼스/사용자 체험의 고려로부터, Effect Hook 을 우선 사용하여 사용하는 것을 권장합니다
特殊的, 대응하는 클린업 작업이 필요한 부수 효과가 있습니다. 예를 들어 외부 데이터 소스의 구독을 취소 (메모리 누수 회피):
class FriendStatus extends React.Component {
/* state 초기화 부분을 생략 */
componentDidMount() {
// 구독
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
// 구독 취소
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
/* render 부분, 및 handleStatusChange 를 생략 */
}
다음과 동등:
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// 구독
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 구독 취소
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
상예와 같이, Effect Hook 은 Disposable 메커니즘을 제공하여 클린업 조작을 서포트하지만, Hooks 의运行机制으로 인해每次 render 후에 클린업 작업이 트리거 됩니다:
Effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
반복 구독이 퍼포먼스에 영향을 미치는 경우, 마찬가지로 의존 관계를 선언함으로써 해결할 수 있습니다 (장래에는 컴파일 시 자동으로 의존 관계를 찾을 가능성이 있습니다)
게다가, 여러 번 useState() 하는 것과 마찬가지로, 여러 번 useEffect() 를 사용하여 다른 Effect 를 분리할 수 있습니다:
Just like you can use the State Hook more than once, you can also use several effects. This lets us separate unrelated logic into different effects.
예를 들어:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
// DOM 조작 Effect
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
// 데이터 구독 Effect
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
static getDerivedStateFromProps()
getDerivedStateFromProps는componentWillReceiveProps를 대체하기 위해 사용되며, state 가 props 의 변화와 연관될 필요가 있는 시나리오에 대응합니다
([二。getDerivedStateFromProps 를 어떻게 이해하는가](/articles/从 componentwillreceiveprops 说起/#articleHeader2) 에서 발췌)
함수형 컴포넌트에서는, props 의 변화가 state 의 변화를 일으키는 시나리오는, State Hook 을 통해 직접 완료할 수 있습니다. 예를 들어 스크롤 방향의 기록:
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// state 에 변경이 없음을 나타내기 위해 null 을 반환
return null;
}
}
다음과 동등:
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// 마지막 렌더링 이후 행이 변경되었습니다. isScrollingDown 을 업데이트.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
shouldComponentUpdate()
함수형 컴포넌트에서는, [React.memo](/articles/react-16-6 新 api/#articleHeader2) 로 대체
getSnapshotBeforeUpdate()
暫時 (2019/06/23) 대체 가능한 Hooks API 는 없지만, 곧 추가됩니다
UNSAFE_componentWillReceiveProps()
前述와 같이, componentWillReceiveProps 와 getDerivedStateFromProps 는 모두 State Hook 으로 대체. constructor() 부분을 참조
UNSAFE_componentWillUpdate()
componentWillUpdate 는 일반적으로 componentDidUpdate 로 대체할 수 있습니다. DOM 상태를 읽을 필요가 있는 경우, getSnapshotBeforeUpdate 로 대체:
Typically, this method can be replaced by componentDidUpdate(). If you were reading from the DOM in this method (e.g. to save a scroll position), you can move that logic to getSnapshotBeforeUpdate().
따라서, componentWillUpdate 는 일반적으로 Effect Hook 또는 LayoutEffect Hook 으로 대체할 수 있습니다. componentDidMount() 부분을 참조
componentDidUpdate()
前述와 같이, componentDidUpdate 는 Effect Hook 으로 대체할 수 있습니다. componentDidMount() 부분을 참조
componentWillUnmount()
前述와 같이, componentWillUnmount 는 Effect Hook 으로 대체할 수 있습니다. componentDidMount() 부분을 참조
static getDerivedStateFromError()
暫時 (2019/06/23) 대체 가능한 Hooks API 는 없지만, 곧 추가됩니다
componentDidCatch()
暫時 (2019/06/23) 대체 가능한 Hooks API 는 없지만, 곧 추가됩니다
Context
함수형 컴포넌트에서도 Context 에 액세스할 수 있으며, 읽기 방식이 더 간단합니다:
// 선언
const {Provider, Consumer} = React.createContext(defaultValue);
// 쓰기
<Provider value={/* some value */}>
// 읽기
<Consumer>
{value => /* context 값에 기반하여 무언가를 렌더링 */}
</Consumer>
다음과 동등:
// 선언
const MyContext = React.createContext(defaultValue);
const Provider = MyContext.Provider;
// 쓰기
<Provider value={/* some value */}>
// 읽기
const value = useContext(MyContext);
Ref
Ref 도 유사한 서포트를 제공합니다:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return <input type="text" ref={this.inputRef} />;
}
}
다음과 동등:
function MyComponent() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 는 마운트된 텍스트 입력 요소를 가리킴
inputEl.current.focus();
};
return <input type="text" ref={inputRef} />;
}
즉:
const refContainer = useRef(initialValue);
// 다음과 동등
const refContainer = React.createRef();
Instance Variables
흥미롭게도, Ref 는컴포넌트 인스턴스 상태를 보유 하는 데에도 사용할 수 있습니다 (this.xxx 에 상당). 예를 들어:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
}
더 나아가, this.mounted 를 통해 componentDidUpdate 를 구현할 수 있습니다:
function FunctionComponent(props) {
// 엄격한 Update 라이프사이클용 플래그
const mounted = useRef();
useEffect(() => {
if (mounted.current) {
// componentDidUpdate
}
});
useEffect(() => {
mounted.current = true;
}, []);
// ...
}
Instance Method
Ref 를 통해 Class 컴포넌트 인스턴스를 참조하고, 그 인스턴스 메서드에 액세스할 수 있습니다:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
// 인스턴스 메서드
focus() {
this.inputRef.current.focus();
}
render() {
return <input type="text" ref={this.inputRef} />;
}
}
class App extends React.Component {
componentDidMount() {
this.myComponent.focus();
}
render() {
return <MyComponent ref={ins => this.myComponent = ins} />;
}
}
다음과 동등:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
class App extends React.Component {
componentDidMount() {
this.myComponent.focus();
}
render() {
return <FancyInput ref={ins => this.myComponent = ins} />;
}
}
아직 댓글이 없습니다