5일차 스터디 - React Essential
2일차에 연장되어 React에 대해 좀더 자세한 이야기를 다루려고 합니다 또한 React 에서 제공하는 엄청난 기능인 React Hook에 대해 스터디하려하는데 워낙 React Hook는 공부할 내용이 방대해 중요한 몇가지만 다루려 합니다.
State and Lifecycle
지금까지 과제를 하면서 보셨던 state와 생명주기에 대해 스터디 해보려합니다.
Component Lifecycle
모든 컴포넌트는 여러 종류의 “생명주기 메서드”를 가지며, 이 메서드를 오버라이딩하여 특정 시점에 코드가 실행되도록 설정할 수 있습니다. 이 생명주기 도표를 필요할 때마다 활용하면 좋습니다. 아래 목록에서 자주 사용되는 생명주기 메서드를 강조되게 표시했습니다. 나머지 것들은 상대적으로 자주 사용되지 않습니다.
마운트
아래 메서드들은 컴포넌트의 인스턴스가 생성되어 DOM 상에 삽입될 때에 순서대로 호출됩니다.
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
업데이트
props 또는 state가 변경되면 갱신이 발생합니다. 아래 메서드들은 컴포넌트가 다시 렌더링될 때 순서대로 호출됩니다.
static getDerivedStateFromProps()
shouldComponentUpdate()
**render()**
getSnapshotBeforeUpdate()
**componentDidUpdate()**
마운트 해제
아래 메서드는 컴포넌트가 DOM 상에서 제거될 때에 호출됩니다.
**componentWillUnmount()**
오류 처리
아래 메서드들은 자식 컴포넌트를 렌더링하거나, 자식 컴포넌트가 생명주기 메서드를 호출하거나, 또는 자식 컴포넌트가 생성자 메서드를 호출하는 과정에서 오류가 발생했을 때에 호출됩니다.
static getDerivedStateFromError()
componentDidCatch()
자주 사용되는 생명주기 메서드
- render()
- 클래스 컴포넌트에서 반드시 구현돼야하는 유일한 메서드입니다.
- 이 메서드가 호출되면
this.props
와this.state
의 값을 활용하여 아래의 것 중 하나를 반환해야 합니다.- React Element: 보통 JSX를 사용하여 생성됩니다.
- 배열과 Fragment: 여러개의 Element를 반환합니다.
- Portal: 별도의 DOM하위 트리에 자식 엘리먼트를 반환합니다.
- 문자열과 숫자: DOM 상에 텍스트 노드로서 렌더링됩니다.
- Boolean 또는 null: 아무것도 렌더링하지 않습니다.
(대부분의 경우
return test && <Chlid />
패턴을 지원하는데에 사용되며, 여기서 test는 Boolean 값입니다.)
- 이 메서드는 순수해야 합니다. 즉, 컴포넌트의 state를 변경하지 않고, 호출될 때마다 동일한 결과를 반환해야 하며, 브라우저와 직접적으로 상호작용을 하지 않습니다.
- constructor()
constructor(props)
- 메서드를 바인딩하거나
state
를 초기화하는 작업이 없다면 생성자를 구현하지 않아도 됩니다. - React 컴포넌트의 생성자는 해당 컴포넌트가 마운트되기 전에 호출됩니다.
React.Component
를 상속한 컴포넌트의 생성자를 구현할 때에는 다른 구문에 앞서super(props)
를 호출해야 합니다. 그렇지 않으면this.props
가 생성자 내에서 정의되지 않아 버그로 이어질 수 있습니다. - React에서 생성자는 보통 아래의 두 가지 목적을 위하여 사용됩니다:
this.state
에 객체를 할당하여 지역 state를 초기화- 인스턴스에 이벤트 처리 메서드를 바인딩
constructor()
내부에서setState()
를 호출하면 안 됩니다. 컴포넌트에 지역state
가 필요하다면 생성자 내에서this.state
에 초기state
값을 할당하면 됩니다.- 생성자는
this.state
를 직접 할당할 수 있는 유일한 곳입니다. 그 외의 메서드에서는this.setState()
를 사용해야 합니다.
- componentDidMount()
- 컴포넌트가 마운트된 직후, 즉 트리에 삽입된 직후에 호출됩니다.
- DOM 노드가 있어야 하는 초기화 작업은 이 메서드에서 이루어지면 됩니다.
- 외부에서 데이터를 불러와야 한다면, 네트워크 요청을 보내기 적절한 위치입니다.
- componentDidUpdate()
- 갱신이 일어난 직후에 호출됩니다.
- 이 메서드는 최초 렌더링에서는 호출되지 않습니다.
- 이 메서드에서
setState()
를 즉시 호출할 수도 있지만,props
비교를 조건문으로 감싸지 않으면 무한 반복이 발생할 수 있다는 점에 주의하세요. 또한 추가적인 렌더링을 유발하여, 비록 사용자는 눈치채지 못할지라도 컴포넌트 성능에 영향을 미칠 수 있습니다.
- componentWillUnmount()
- 컴포넌트가 마운트 해제되어 제거되기 직전에 호출됩니다.
- 이 메서드 내에서 타이머 제거, 네트워크 요청 취소, componentDidMount() 내에서 생성된 구독 해제 등 필요한 모든 정리 작업을 수행하세요.
- 이제 컴포넌트는 다시 렌더링되지 않으므로, componentWillUnmount() 내에서 setState()를 호출하면 안 됩니다. 컴포넌트 인스턴스가 마운트 해제되고 나면, 절대로 다시 마운트되지 않습니다.
State를 올바르게 사용하기
직접 state를 수정하지 마세요
// Wrong
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});
State 업데이트는 비동기적일 수도 있습니다.
React는 성능을 위해 여러 setState()
호출을 단일 업데이트로 한꺼번에 처리할 수 있습니다.
this.props
와 this.state
가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 됩니다.
// 다음 코드는 카운터 업데이트에 실패할 수도 있습니다.
this.setState({
counter: this.state.counter + this.props.increment,
});
// 이를 수정하기 위해서는 함수를 이용해 이전 값이 들어있는
// state, props를 인자로 받아와서 사용합니다.
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
State 업데이트는 병합됩니다
setState()
를 호출할 때 React는 제공한 객체를 현재 state
로 병합합니다.
state
는 다양한 독립적인 변수를 포함할 수 있습니다.- 별도의
setState()
호출로 이러한 변수를 독립적으로 업데이트할 수 있습니다. - 병합은 얕게 이루어지기 때문에
this.setState({comments})
는this.state.posts
에 영향을 주진 않지만this.state.comments
는 완전히 대체됩니다.
데이터는 아래로 흐릅니다
부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트가 유상태인지 또는 무상태인지 알 수 없고, 그들이 함수나 클래스로 정의되었는지에 대해서 관심을 가질 필요가 없습니다.
이 때문에 state는 종종 로컬 또는 캡슐화라고 불립니다. state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없습니다.
컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있습니다.
// 자신의 state를 자식 컴포넌트에 전달
<FormattedDate date={this.state.date} />
// props로 전달 받은 값 사용
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
일반적으로 이를 “하향식(top-down)” 또는 “단방향식” 데이터 흐름이라고 합니다. 모든 state
는 항상 특정한 컴포넌트가 소유하고 있으며 그 state
로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 “아래”에 있는 컴포넌트에만 영향을 미칩니다.
이벤트 처리하기
<button onClick={clickHandler()}>Click Me!</button>
/*
기존 DOM Element 와 유사하지만 이렇게 하면
click을 안해도 계속해서 clickHandler() 메서드가 호출되게 됩니다.
React에서는 아래와 같이 이벤츠를 처리해야 합니다/
*/
<button onClick={(event) => clickHandler(event)}>Click Me!</button>
// 또는
<button onClick={clickHandler}>Click Me!</button>
또한 React에서는 Event Handler에서 false
를 반환해도 기본 동작을 방지할 수 없습니다.
반드시 event.preventDefault()
를 명시적으로 호출해야 합니다.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 콜백에서 `this`가 작동하려면 constructor() 에서 바인딩을 해주어야 합니다.
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
// 화살표 함수를 사용하면 바인딩을 하지않아도 자동으로 `this`가 바인딩됩니다.
noBinding = () => {
// 바인딩없이 바로 `this` 사용 가능
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}))
}
render() {
return (
// 또한 onClick={(e) => this.handleClick(e)} 으로 사용시에도 바인딩 없이 `this`가 바인딩됩니다.
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
Form 데이터 받기
input, select 등 데이터를 받아오는 작업을 해야할때 이 value 값은 state로 관리해야 합니다.
ref 를 이용한 방법도 있지만 권장되지는 않습니다.
class FlavorForm extends React.Component {
// constructor에서 초기화 하는것과 같음
state = {
value: ''
}
setInput = (event) => {
this.setState({value: event.target.value});
}
render() {
return <input value={this.state.value} onChange={setInput}/>;
}
}
위의 예시처럼 input 태그의 value 값이 변경될때마다 state를 변경해줘야 반영이되고
다른곳에서 이 state의 값을 그대로 사용할 수 있습니다.
React Hook
Hook은 React 버전 16.8부터 React 요소로 새로 추가되었습니다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있습니다.
Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. Hook은 class 안에서는 동작하지 않습니다. 대신 class 없이 React를 사용할 수 있게 해주는 것입니다.
Class Component 에서 Function Component 로 변경
기존까지 Class 바탕의 Component를 만들었습니다. 왜냐하면 생명주기에 따른 데이터 처리나 state를 관리하려면 어쩔 수 없는 선택이었기 때문입니다.
React 16.8 이전에는 React Hook이라는 개념이 없어 데이터를 다루는 Component는 무조건 class 방식으로 만들어야 했습니다. 그러나 React 16.8부터 Function 방식의 Component에서도 상태를 다루고 생명주기를 다룰 수 있게되어 더욱 편리하게 Component를 생성할 수 있게 되었습니다.
State Hook
버튼을 클릭하면 값이 증가하는 간단한 카운터 예시입니다.
import React, { useState } from 'react';
function Example() {
// "count"라는 새로운 state 변수를 선언합니다
// state 변수와 state의 값을 변경하는 요소 두개가 들어있는 배열이 반환됩니다.
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
여기서 userState()
가 바로 Hook 입니다.
Hook을 호출해 함수 컴포넌트(function component) 안에 state
를 추가했습니다.
이 state
는 컴포넌트가 다시 렌더링 되어도 그대로 유지될 것입니다.
useState()
는 현재의 state
값과 이 값을 업데이트하는 함수를 쌍으로 제공합니다.
이것은 class의 this.setState
와 거의 유사하지만, 이전 state
와 새로운 state
를 합치지 않는다는 차이점이 있습니다.
useState()
는 인자로 초기 state 값을 하나 받습니다.
카운터는 0부터 시작하기 때문에 위 예시에서는 초기값으로 0을 넣어준 것입니다.
Effect Hook
React의 생명주기를 수행할 수 있게 해주는 Hook입니다.
React class의 componentDidMount
나 componentDidUpdate
, componentWillUnmount
와 같은 목적으로 제공되지만, 하나의 API로 통합된 것입니다.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 비슷합니다
// 모든 생명주기 메서드를 useEffect 하나로 처리한다고 생각하면 됩니다.
useEffect(() => {
// 브라우저 API를 이용해 문서의 타이틀을 업데이트합니다
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
위 예시는 React가 DOM을 업데이트한 뒤에 문서의 타이틀을 바꾸는 컴포넌트입니다.
useEffect를 사용하면, React는 DOM을 바꾼 뒤에 “effect” 함수를 실행할 것입니다. Effects는 컴포넌트 안에 선언되어있기 때문에 props와 state에 접근할 수 있습니다. 기본적으로 React는 매 렌더링 이후에 effects를 실행합니다.
useEffect는 다음과 같이 사용됩니다.
// 컴포넌트가 Mount됐을때 한번, 다른 요소들이 update될때마다 계속 callback 함수가 실행된다.
useEffect(callback)
// 컴포넌트가 Mount됐을때만 한번 callback 함수가 실행된다.
useEffect(callback, [])
// 컴포넌트가 Mount됐을때 한번, obj 라는 요소가 update될때마다 계속 callback 함수가 실행된다.
useEffect(callback, [obj])
2, 3번째를 보면 2번째 매개변수로 배열을 받고있습니다. 이 배열안에 있는 요소를 참조하여 그 요소의 update만을 필터링해 callback함수를 실행하게됩니다. 이때 만약 배열이 비어있으면 한번도 update하지 않습니다.
useEffect의 반환타입
useEffect의 반환타입은 void 또는 callback 함수입니다.
// 기본적으로 void로 사용되며
// callback 함수를 return하게 된다면 그 callback 함수는 Component가 Unmount될때 실행됩니다.
useEffect(() => {
return () => {
// 여기있는 코드는 Unmount될대 실행됩니다.
}
}, []);
Hook 사용 규칙
Hook은 그냥 JavaScript 함수이지만, 두 가지 규칙을 준수해야 합니다.
- 최상위(at the top level)에서만 Hook을 호출해야 합니다. 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하지 마세요.
- React 함수 컴포넌트 내에서만 Hook을 호출해야 합니다. 일반 JavaScript 함수에서는 Hook을 호출해서는 안 됩니다. (Hook을 호출할 수 있는 곳이 딱 한 군데 더 있습니다. 바로 직접 작성한 custom Hook 내입니다. 이것에 대해서는 나중에 알아보겠습니다.)
이 규칙들을 강제하기 위해서 linter plugin을 제공하고 있습니다. 이 규칙들이 제약이 심하고 혼란스럽다고 처음에는 느낄 수 있습니다. 하지만 이것은 Hook이 제대로 동작하기 위해서는 필수적인 조건입니다.
나만의 Hook 만들기(Custom Hook)
개발을 하다 보면 가끔 상태 관련 로직을 컴포넌트 간에 재사용하고 싶은 경우가 생깁니다. 이 문제를 해결하기 위한 전통적인 방법이 두 가지 있었는데, higher-order components와 render props가 바로 그것입니다. Custom Hook은 이들 둘과는 달리 컴포넌트 트리에 새 컴포넌트를 추가하지 않고도 이것을 가능하게 해줍니다.
여러 Hook을 이용하여 하나의 기능을 제공하는 Custom Hook을 만드는것입니다.
import React, { useState, useEffect } from 'react';
// Custom Hook
function useFriendStatus(friendID) {
// 친구의 Online 여부를 state로 관리합니다.
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
// 상태관리되는 state를 반환합니다.
return isOnline;
}
위의 예시는 친구의 Online 여부를 계속해서 update하는 Custom Hook입니다.
그렇게 만들어진 Custom Hook은 아래와같이 여러 곳에서 재사용이 가능하며 필요에따라 다른 기능들을 추가적으로 만들어낼 수 있습니다.
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={ isOnline ? 'color: green' : 'color: black'}>
{props.friend.name}
</li>
);
}
위의 각 컴포넌트의 state는 완전히 독립적입니다. Hook은 state 그 자체가 아니라, 상태 관련 로직을 재사용하는 방법입니다. 실제로 각각의 Hook 호출은 완전히 독립된 state를 가집니다. 그래서 심지어는 한 컴포넌트 안에서 같은 custom Hook을 두 번 쓸 수도 있습니다.
과제
오늘 과제는 Custom Hook을 만들어보는 것입니다!
-
useDeviceOrientation
크롬 개발자 도구에서 Sensors 클릭 하여 Orientation을 조정하여 디바이스 각도 표출
-
useFavicon
버튼 클릭시 탭에있는 Favicon이 변경
-
useGeolocation
Client의 Geolocation을 출력
-
useKeyPress
키보드로 입력되는 것들만 출력
-
useLocalStorage
LocalStorage를 사용하여 상태값 변경
-
useMousePosition
마우스의 위치를 출력
-
useOnline
크롬개발자도구의 Network 탭을 들어가서 offline 으로 설정가능하다
client의 online, offline 상태를 체크하여 출력
-
useLockScroll
스크롤을 막거나 푸는 Hook