모든 코드는 git repository에서 확인 가능합니다.
1 리액트 훅 기초 익히기
- 리액트 버전 16.8에서 추가 됨
1.1 리액트 훅이란?
함수형 컴포넌트에서도 클래스형 컴포넌트의 기능을 사용할 수 있게 하는 기능 훅을 통해서 컴포넌트의 상탯값을 관리 할 수 있고, 컴포넌트의 생명 주기 함수를 이용할 수 있다.
-
로직을 재사용하는 기존 방식의 한계
* 리액트에서 로직의 재사용은 주로 '고차 컴포넌트', '렌더 속성값 패턴' 으로 이루어 진다. * 두 방법은 대상이 되는 컴포넌트를 감싸는 새로운 컴포넌트를 생성하기 때문에 리액트 요소 트리가 깊어진다. * 트리가 깊어지면 성능에 부정적 영향, 개발시 디버깅을 힘들게 하는원인이 된다.
-
클래스 형 컴포넌트의 한계
* 서로 연과성이 없는 여러 가지 로직을 하나의 생명 주기 메서드에 작성하는 경우가 많다. (훅에서는 로직별로 구분이 가능하다 코드 5-10 참고) * componentDidMount 메서드에서 등록하고 componentWilUnmoun메서드에서 해제하는 코드가 자주 사용되는데, 등록하고 해제하는것을 깜빡하는 실수를 하기 쉽다. * 함수형 컴포넌트에 상탯값이나 생명주기 함수를 추가 하기 위해 클래스형 컴포넌트로 변경하는 작업도 상당히 귀찮은 작업! => 이유는 클래스형 컴포넌트를 작성할 때 부수적으로 작성해야 하는 코드가 많다. * 클래스 사용시 코드 압축이 잘 안되는 경우가 있고, 핫 리로드에서 난해한 버그를 발생시키고, 컴파일 단계에서 코드를 최적화 하기 어렵게 만든다.
-
훅의 장점
* 훅은 함수 -> 함수 안에서 다른 함수 호출 가능 -> 새로운 훅 만들 수 있다. * 리액트 내장 훅과 다른 사람들이 만든 커스텀 훅을 조립해서 새로운 훅을 만들 수 있다. * 같은 로직을 한곳으로 모을 수 있어서 가독성이 좋다. (클래스형 컴포넌트의 생명 주기 메서드는 서로 다른 로직이 다른 로직이 하나의 메서드에 섞여 있어서 가독성이 좋지 않다. 또는 같은 로직이 componentDidMount와 componentdidUpdate 메서드에 중복으로 들어가기도 한다.) * 함수이기 때문에 클래스형 컴포넌트보다 타입스크립트로 정적 타입 언어 타입을 정의하기 쉽다.
1.2 함수형 컴포넌트에 상탯값 추가하기: useState
- useState 기본예 코드 5-1 useState 훅 사용하기
- ⭐️ 컴포넌트 상탯값 관리
- ⭐️ onChange 속성값으로 입력되는 함수는 렌더링이 될 때 마다 생성되므로 성능이 걱정됨으로 'useCallback' 훅 제공
-
5-24, 25 예제 참고
- 코드 5-24 useCallback 훅이 필요한 예
- 코드 5-25 useCallback 훅 사용하기
import React, { useState } from "react"
export default () => {
const [name, setName] = useState("")
return (
<div>
<h1> 예제 </h1>
<p>{`# useState 테스트 - 나의 이름은 ${name}`}</p>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
</div>
)
}
-
클래스형 컴포넌트와 비교하기
- 코드 5-2 클래스형 컴포넌트에서 상태값 관리하기
- POINT1 초기화: 클래스 멤버 변수로 초기 상탯값을 정의
- POINT2 상탯값 클래스 인스턴스에서 가져옴
- POINT2 상탯값 변경시 'this.setState' 호출
import React from 'react'; export default class Profile extends React.Component { // POINT1 초기화: 클래스 멤버 변수로 초기 상탯값을 정의 state = { name: '' }; render() { // POINT2 상탯값 클래스 인스턴스에서 가져옴 const { name } = this.state; return ( <div> <h1> 예제 </h1> <p>{`# 클래스 컴포넌트 - 나의 이름은 ${name}`}</p> <input type='text' value={name} //POINT2 상탯값 변경시 'this.setState' 호출 onChange={(e) => this.setState({ naem: e.target.value })} /> </div> ); } }
-
여러 개의 useState 훅 사용하기
- 코드 5-3 여러 개의 useState 훅사용하기
- 필요한 만큼 useState 훅을 호출 할 수 있다.
import React, { useState } from 'react'; export default () => { const [ name, setName ] = useState(''); const [ age, setAge ] = useState(0); return ( <div> <h1> 예제 </h1> <p>{`나의 이름은 ${name}, 나이는 ${age}`}</p> 이름 입력: <input type='text' value={name} onChange={(e) => setName(e.target.value)} /> <br /> 나이 입력: <input type='text' value={age} onChange={(e) => setAge(e.target.value)} /> </div> ); };
-
useState 훅 하나로 여러 상탯값 관리하기
- 코드 5-4 하나의 useState 훅으로 여러 상탯값 관리하기
- 두 상태 값을 하나의 객체로 관리 가능
- ⭐️ setState메서드는 기존 상탯값과 입력된 값을 병합하지만
- ⭐️ useState 훅은 이전 상탯값을 지기 때문에 "...state"와 같은 코드가 필요.
- ⭐️ 상탯값을 하나의 객체로 관리하는 경우를 위해 'useReducer' 훅이 제공
import React, { useState } from 'react'; export default () => { const [ name, setName ] = useState(''); const [ age, setAge ] = useState(0); return ( <div> <h1> 예제 </h1> <p>{`나의 이름은 ${name}, 나이는 ${age}`}</p> 이름 입력: <input type='text' value={name} onChange={(e) => setName(e.target.value)} /> <br /> 나이 입력: <input type='text' value={age} onChange={(e) => setAge(e.target.value)} /> </div> ); };
1.3 함수형 컴포넌트에서 생명 주기 함수 이용하기: useEffect
useEffect 두번째 매개변수 모양에 따른 3가지 의미
1. ⭐️ useEffect 두번째 매개변수 '[]'의 의미
- 컴포넌트가 마운트 될 때(=componentDidMounted) 첫번째 매개변수 함수 호출
- 컴포넌트가 언마운트 될 때(=componentWillUnmount) 첫번째 매개변수 함수 호출
2. ⭐️ useEffect 두번째 매개변수 '[value]'의 의미
- 배열의 값이 변경되는 경우에만 함수가 호출
3. ⭐️ useEffect 두번째 매개변수가 없을 때
- 클래스 컴포넌트 "componentDidMount", "componentDidUpdate"두 함수에서 수행하는 것과같다.
- useEffect 기본 예 코드 5-5 useEffect 훅의 사용예
-
⭐️ useEffect에 있는 코드는 클래스형 컴포넌트에서 어떻게 구현할까?
- 클래스형 컴포넌트의 componentDidMount, componentDidUpdate 양쪽 메서드에 추가하면 가은 기능을 하게 된다.
-
⭐️ 버튼을 누르면?
- 다시 렌더링 되고, 렌더링이 끝나면 useEffect 훅에 입력된 함수가 호출
import React, { useState, useEffect } from 'react';
export default () => {
const [ count, setCount ] = useState(0);
//POINT
useEffect(() => {
document.getElementById('count').innerHTML = `업데이트 횟수: ${count}`;
});
return (
<div>
<h1> 예제 </h1>
<div>
<label id='count' />
</div>
<button onClick={() => setCount(count + 1)}>increase</button>
</div>
</div>
);
};
- API를 호출하는 기능(함수형 컴포넌트) 코드 5-6 useEffect 훅에서 API 호출하기
- usdEffect 훅의 두번째 매개변수로 배열을 입력하면, 배열의 값이 변경되는 경우에만 함수가 호출
import React, { useState, useEffect } from "react"
import axios from "axios"
function Profile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
axios("https://api.github.com/users/happyjy").then(Response => {
console.log(Response)
setUser(Response.data)
})
}, [userId])
return (
<div>
<div>
<h1> 예제 </h1>
<div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div>
<div>
{user && (
<p>
{user.name}/ {user.id}
</p>
)}
</div>
</div>
</div>
)
}
export default Profile
-
API를 호출하는 기능(클래스형 컴포넌트)
- 코드 5-7 클래스형 컴포넌트에서 API 호출하기
- 코드 5-6을 클래스형 컴포넌트로 작성
- 첫번째 렌더링 후에는 componentDidMount메서드가 호출, 두번째 렌더링 후부터는 componentDidUpdate 메서드가 호출 되므로 API와 통신하는 코드를 양쪽에서 작성해야한다.
- 단 componentDidUpdate 메서드에서는 userId가 변경된 경우에만 API를 호출
- 이 처럼 클래스형 컴포넌트에서는 중복된 코드가 여러 생명주기 메서드에 흩어져 있는 경우가 많다.
import React from 'react'; import axios from 'axios'; class Profile extends React.Component { state = { user: null }; componentDidMount() { const { userId } = this.props || { userId: 'happyjy' }; axios('https://api.github.com/users/happyjy').then((Response) => { console.log(Response); this.setState({ user: Response.data }); }); } componentDidUpdate(prevProps) { const { userId } = this.props || { userId: 'happyjy' }; if (userId !== prevProps.userId) { axios('https://api.github.com/users/happyjy').then((Response) => { console.log(Response); this.setState({ user: Response.data }); }); } } render() { const { user } = this.state; return ( <div> <div> <h1> 예제 </h1> <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div> <div> {user && ( <p> {user.name}/ {user.id} </p> )} </div> </div> </div> ); } } export default Profile;
-
이벤트 처리 함수를 등록하고 해제하는 기능: 함수형 컴포넌트로 작성하기
- 코드 5-8 useEffect 훅을 이용해서 이벤트 여러 함수를 등록하고 해제하기
-
POINT2: useEffect 첫번째 매개변수
- ⭐️ useEffect 첫번째 매개변수에 등록된 함수가 함수를 반환할 수 있다.
- ⭐️ 반환된 함수는 컴포넌트가 언마운트 되거나 첫번째 매개변수로 입력된 함수가 호출 되기 직전에 호출된다.
-
POINT3: useEffect 두번째 매개변수
- ⭐️ 빈 배열을 넣으면
컴포넌트가 마운트 될 때
,컴포넌트가 언마운트 될때
첫번째 매개변수로 입력된 함수가 호출
⭐️ 클래스형 컴포넌트의 componentDidMount, componentWillUnmount 메서드에서만 실행되는 것과같은 효과가 있다.
- ⭐️ 빈 배열을 넣으면
import React, { useState, useEffect } from "react" export default function() { const [width, setWidth] = useState(window.innerWidth) useEffect(() => { const onResize = () => setWidth(window.innerWidth) window.addEventListener("resize", onResize) //POINT1: // POINT2: return () => { window.removeEventListener("resize", onResize) } }, []) // POINT3: return ( <div> <div> <h1> 예제 </h1> {`width is ${width}`} </div> </div> ) }
-
이벤트 처리 함수의 등록하고 해제하는 기능: 클래스형 컴포넌트로 작성하기
- 코드 5-9 이벤트 처리 함수의 등록과 해제를 클래스형 컴포넌트로 작성하기
import React from 'react'; class WidthPrinter extends React.Component { state = { width: window.innerWidth }; componentDidMount() { window.addEventListener('resize', this.onResize); } componentWillMount() { window.removeEventListener('resize', this.onResize); } onResize = () => { this.setState({ width: window.innerWidth }); }; render() { const { width } = this.state; return ( <div> <div> <h1> 예제 </h1> {`width is ${width}`} </div> </div> ); } } export default WidthPrinter;
-
두 가지 기능을 합치기: 함수형 컴포넌트에서 작성하기
- 코드 5-10 훅을 사용하면 로직별로 코드를 모을 수 있다.
- 코드 5-06, 08 API호출, windowWidth 코드를 합쳐보자!
- ⭐️ 각각의 로직이 다른 useEffect에 있어 가독성이 좋다.
import React, { useState, useEffect } from 'react'; import axios from 'axios'; export default ({ userId = 'jyoon ' }) => { const [ user, setUser ] = useState(null); useEffect( () => { axios('https://api.github.com/users/happyjy').then((Response) => { console.log(Response); setUser(Response.data); }); }, [ userId ] ); const [ width, setWidth ] = useState(window.innerWidth); useEffect(() => { const onResize = () => setWidth(window.innerWidth); window.addEventListener('resize', onResize); //POINT1: // POINT2: return () => { window.removeEventListener('resize', onResize); }; }, []); // POINT3: return ( <div> <div> <h1> 예제 </h1> <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div> <div> {user && ( <p> {user.name}/ {user.id} </p> )} </div> <div>{`width is ${width}`}</div> </div> </div> ); };
-
클래스형 컴포넌트에서는 로직이 분산된다.
- 코드 5-11 클래스형 컴포넌트에서는 로직이 분산된다.
import React from 'react'; import axios from 'axios'; class Profile extends React.Component { state = { user: null, width: window.innerWidth }; componentDidMount() { const { userId } = this.props || { userId: 'happyjy' }; axios('https://api.github.com/users/happyjy').then((Response) => { console.log(Response); this.setState({ user: Response.data }); }); window.addEventListener('resize', this.onResize); } componentDidUpdate(prevProps) { const { userId } = this.props || { userId: 'happyjy' }; if (userId !== prevProps.userId) { axios('https://api.github.com/users/happyjy').then((Response) => { console.log(Response); this.setState({ user: Response.data }); }); } } componentWillMount() { window.removeEventListener('resize', this.onResize); } onResize = () => { this.setState({ width: window.innerWidth }); }; render() { const { user, width } = this.state; return ( <div> <h1> 예제 </h1> <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div> <div> {user && ( <p> {user.name}/ {user.id} </p> )} </div> <div>{`width is ${width}`}</div> </div> </div> ); } } export default Profile;
1.4 훅 직접 만들기
훅을 직접 만들어서 사용하면 고차 컴포넌트와 렌더 속성값 패턴 처럼 로직을 재사용할 수 있다.
-
useWindowWidth 커스텀 훅
- 코드 5-12 useWindowWidth 커스텀 훅
import React, { useState, useEffect } from 'react'; import useWindowWidth from './customHook/useWindowWidth'; export default () => { const windowWidth = useWindowWidth(); return ( <div> <div> <h2> 결과 </h2> <p>this is widowWidth: {windowWidth}</p> <p>this is widowWidth: {windowWidth}</p> </div> </div> ); };
-
POINT1
- ⭐️ useState, useEffect훅을 이용해서 커스텀 훅을 만들었다.
- ⭐️ 이렇게 레고 블록처럼 기존 훅을 이용해서 새로운 훅을 만들 수 있다는 점이 훅의 매력이다.
-
POINT2
- ⭐️ 창의 너비를 저장해서 제공하는 것이 useWindowWidth 훅의 역할
-
useWindowWidth 훅 사용하기
- 코드 5-13 useWindowWidth 훅 사용하기
- 창너비 조절시 다시 렌더링되어 화면에 나타난다.
- ⭐️ 커스텀 훅은 리액트의 내장 훅과 같은 방식으로 사용될 수 있다.
- 커스텀 훅과 내장 훅을 함께 사용하는 것도 가능
import React, { useState } from 'react'; import useWindowWidth from './customHook/useWindowWidth'; export default function() { const width = useWindowWidth(); const [ name, setName ] = useState(''); return ( <div> <div> <h1> 예제 </h1> <p>{`name is ${name}`}</p> <p>{`winddow width is ${width}`}</p> {width < 600 && <div>width가 600보다 작아요</div>} {width >= 600 && <div>width가 600보다 크거나 같아요</div>} <input type='text' value={name} onChange={(e) => setName(e.target.value)} /> </div> </div> ); }
-
⭐️ 예로 부터 useWindowWidth 훅에 사용으로 미루어 커스텀 훅의 장점
- 공통된 동작을 따로 빼서 다른 컴포넌트에도 똑같이 적용할 수 있겠다.
- ⭐️ componentDidMount 시점부터 적용하기기 위해서는 어떻게 해야할까?
-
useHasMounted 커스텀 훅
다음 두 코드 컴포넌트 mount여부에 따라서 구현한 커스텀 훅, 고차 컴포넌트를 비교 했을때 두가지 비교사항이 있다.
- 코드 5-14 useHasMounted 커스텀 훅
- 코드 5-15 withHasMounted 고차 컴포넌트
- 훅이 고차 컴포넌트보다 간결하게 코드 작성가능
- 타입스크립트 같이 정적 타입 언어를 사용하는 경우 고차 컴포넌트의 타입을 정의하는 것이 여간 귀찮은 작업!, but 훅은 일반 함수이기 때문에 쉽게 타입을 정의할 수 있다.
* 코드 5-14 useHasMounted 커스텀 훅 ```js import React, { useState, useEffect } from "react"; import useHasMounted from "./customHook/useHasMounted"; export default function () { const hasMounted = useHasMounted(); return ( <div> <div> <h1> 예제 </h1> <p>{`this is hasMounted: ${hasMounted}`}</p> </div> </div> ); } ``` * 컴포넌트 마운트 여부를 알려주는 훅이다 * ⭐️ useHasMounted 훅은 컴포넌트가 마운트 된 후에 참 반환 * ⭐️⭐️ useEffect 훅의 두 번째 매개변수에 빈 배열을 넣었기 때문에 업데이트 하는 경우에는 setHasMounted 함수가 호출 되지 않는다. * ⭐️ useEffect 두번째 매개변수 '[]'의 의미 - `컴포넌트가 마운트 될 때`, `컴포넌트가 언마운트 될 때` 첫번째 매개변수로 입력된 함수가 호출함수 return 함수 호출 * ⭐️ useEffect 두번째 매개변수 '[value]'의 의미 - 배열의 값이 변경되는 경우에만 함수가 호출 * ⭐️ useEffect 두번째 매개변수가 없을 때 - 클래스 컴포넌트 "componentDidMount", "componentDidUpdate"두 함수에서 수행하는 것과같다. * 코드 5-15 withHasMounted 고차 컴포넌트 ```js import React, { Component } from "react"; function withHasMounted(InputComponent) { return class OutputComponent extends Component { state = { hasMounted: false, }; componentDidMount() { setTimeout(() => { this.setState({ hasMounted: true }); }, 1000); } render() { const { hasMounted } = this.state; return ( <InputComponent {...this.props} hasMounted={hasMounted} ></InputComponent> ); } }; } class Test extends Component { state = {}; render() { console.log(this.props); //AppHook에서 넘겨준 value + withHasMounted에서 받은 props. const { hasMounted } = this.props; return ( <div> <div> <h1> 예제 </h1> {!hasMounted && <div>마운트 되기 전입니다. 1초뒤 마운트 됩니다.</div>} {hasMounted && <div>1초뒤 마운트 됐다 !!!</div>} </div> </div> ); } } export default withHasMounted(Test); ``` * 5-14 hook으로 되어 있는 기능을 "고차 컴포넌트로 작성한 코드" * ⭐️ 타입 스크립트와 같은 정적 타입 언어를 사용하는 경우 - 고차 컴포넌트의 타입을 정의하는 작업 -> 손이 많이감. - 반면 훅은 일반 함수 -> 쉽게 타입 정의 가능
1.5 훅 사용 시 지켜야 할 규칙
-
코드 5-16 훅 사용 시 규칙을 위한반 경우
import React, { useState } from "react" export default function() { const [value, setValue] = useState(0) if (value === 0) { const [v1, setV1] = useState(0) } else { const [v1, setV1] = useState(0) const [v2, setV2] = useState(0) } return ( <div> <p> 코드 5-16 훅 사용 시 규칙을 위한반 경우</p> </div> ) }
- ⭐️ 1. 조건에 따라 호출하면
- 에러는 안나는데 순서가 보장 되지 않는다.
- ⭐️ 2. for문 안에서는 아래와 같은 에러를
- React Hook "useState" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render
- ⭐️ 3. 일반 function 안에서 호출할 시
- React Hook "useState" is called in function "func" which is neither a React function component or a custom React Hook function
- 훅의 호출 순서가 같아야 하는이유
-
코드 5-17 여러 개의 훅 사용하기
- ⭐️ 아래 예제로 "훅의 호출 순서가 같아야 하는 이유"를 알아보자.
import React, { useState, useEffect } from "react" export default function() { const [age, setAge] = useState(0) //POINT1 const [name, setName] = useState("") //POINT2 useEffect(() => { setAge(31) //POINT3 }, []) return ( <div> <h1>예제</h1> this is age: {age}, this is name: {name} </div> ) }
- ⭐️ useState훅에 전달한 정보는 상탯값의 기본값밖에 없다.
=> ⭐️ 리액트가 age, name상탯값을 구분할 수 있는 유일한 정보는 훅이 사용된 순서 - 추측!
- ⭐️ useState하면 생성되는 데이터 들이 stack 으로 관리 된다라고 추측
- 예제 5-18 리액트 내부코드 예제를 보면 배열로 관리 되고 있다.
- 리액트가 내부적으로 훅을 처리하는 방식
-
코드 5-18 의사코드로 표현한 리액트의 내부 코드
import React from "react" export function useHook() { //POINT1 //... const hookData = {} //임의 useHook.push(hookData) //POINT2 } //POINT3 function process_a_component_rendering(component) { hooks = [] //POINT4 component() //POINT5 let hooksForThisComponent = hooks //POINT6 hooks = null //POINT6 //... } export default function() { return ( <div> <p> 코드 5-18 의사코드로 표현한 리액트의 내부 코드</p> </div> ) }
- POINT1
- 리액트가 내장하고 있는 useState, useEffect와 같은 훅
- POINT2
- ⭐️ 각 훅 함수에서는 hooks 배열에 자신이 데이터를 추가
- POINT3
- 렌더링 과정에서 하나의 컴포넌트를 처리하는 함수
- POINT4
- hooks를 빈 매열로 초기화
- POINT5
- 컴포넌트 내부에서 훅을 사용한 만큼 hooks 배열에 데이터가 추가
- POINT6
- 생성된 배열을 저장하고 hooks 변수를 초기화 한다.
- 이처럼 사용된 순서를 저장하고 배열에 저장된 수서를 기반으로 훅을 관리