useState, useEffect 훅에 대해서는 이전 포스팅에서 다루어 보았기 때문에 그 이외의 훅에 대해서 좀 더 자세하게 알아보겠습니다.
우선 useRef 에 대해 알아보겠습니다.
useRef 는 꼭 돔의 참조객체만을 다룰 필요는 없습니다. 아래와 같이 렌더링과 상관 없는 값을 다루는 경우에도 유용하게 사용할 수 있습니다. timerId 라는 값을 useState 로 다룬다면 timerId 라는 값이 변경될 때마다 컴포넌트가 다시 렌더링이 됩니다. 하지만 렌더링에 관련된 데이터가 아니기 때문에 컴포넌트를 렌더링 할 필요가 없습니다.
이 때문에, 렌더링과 무관한 데이터들은 useRef 를 사용하여 다루어 주는 것이 좋습니다.
import React, { userState, useRef, useEffect } from 'react';
export default function App() {
const timerIdRef = useRef(-1);
useEffect((0 => {
timerIdRef.current = setTimeout(() => {}, 1000)
});
useEffect((0 => {
if(timerIdRef.current >= 0) {
clearTimeout(timerIdRef.current);
}
});
}
useMemo는 계산량이 많은 함수의 반환값을 재활용하는 용도로 사용됩니다. 여기서는 runExpensiveJob 함수가 계산량이 많다고 가정을 해보겠습니다. useMemo 안에 첫번째 매개변수로 runExpensiveJob 라는 복잡한 계산을 하는 함수를 입력합니다. 그러면 리액트가 함수의 반환값을 기억을 할 겁니다. 또한 이 함수는 v1, v2 값이 하나라도 변경되면 실행될 예정입니다.
만약 v1, v2 값이 변경되지 않았다면 이전에 실행된 함수의 결과값을 재활용 하는 것입니다. useMemo의 두번째 인자는 useEffect와 마찬가지로 의존성 배열입니다. 따라서, v3이 변경되더라도 runExpensiveJob 함수는 실행되지 않게 됩니다.
import React, { userState, useMemo } from 'react';
export default function App() {
const [v1, setV1] = useState(0);
const [v2, setV2] = useState(0);
const [v3, setV3] = useState(0);
const value = useMemo(() => runExpensiveJob(v1, v2), [v1, v2]);
return (
<>
<p>{`value is ${value}`}</p>
<button
onClick={() => {
setV1(Math.random());
setV2(Math.random());
}}
>
v1/v2 수정
</button>
<p>{`v3 is ${v3}`}</p>
<button onClick={() => setV3(Math.random())}>v3 수정</button>
</>
)
}
function runExpensiveJob(v1, v2) {
return v1 + v2;
}
위에서 설명한 useMemo과 비슷하고 메모이제이션을 이용을 합니다. 두드러지는 특징이라고 한다면, 함수 메모이제이션에 특화된 훅이라고 생각하시면 되겠습니다.
아래의 UserEdit 컴포넌트를 보면 React.memo 컴포넌트를 사용했음에도 불구하고 다시 렌더링 되는 것을 볼 수 있습니다.
그 이유는, 새로운 함수가 만들어져서 전달되었기 때문입니다. React.memo의 기능은 동일한 속성값을 전달받는 경우에는 리렌더링 하지 않도록 도와주는 기능입니다. 하지만 동일한 함수임에도 새로운 함수라고 인식되었다면 React.memo의 기능이 적용되지 않습니다.
import React, { userState } from 'react';
export default function App() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [v1, setV1] = useState(0);
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
<UserEdit
onSave={() => saveToServer(name, age)}
setName={setName}
setAge={setAge}
/>
<p>{`v1 is ${v1}`}</p>
<button onClick={() => setV1(Math.random())}>v1 수정</button>
</div>
)
}
const UserEdit = React.memo(function ({ onSave, setName, setAge }) {
console.log('UserEdit render');
return null;
})
function saveToServer(name, age) {}
따라서, useCallback 훅을 사용하여 함수를 아래와 같이 고정해 둘 필요가 있습니다. useCallback도 의존성 배열로 관리를 합니다. name, age 값이 변경될 때만 새로운 함수가 생성되도록 작성을 했습니다.
import React, { userState, useCallback } from 'react';
export default function App() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [v1, setV1] = useState(0);
const onSave = useCallback(() => saveToServer(name, age), [name, age]);
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
<UserEdit
onSave={onSave}
setName={setName}
setAge={setAge}
/>
<p>{`v1 is ${v1}`}</p>
<button onClick={() => setV1(Math.random())}>v1 수정</button>
</div>
)
}
const UserEdit = React.memo(function ({ onSave, setName, setAge }) {
console.log('UserEdit render');
return null;
})
function saveToServer(name, age) {}
useReducer는 여러개의 상태값을 한번에 사용하는 경우에 적합한 리액트 훅입니다.
이 훅을 사용하기 위해서는 아래와 같이 리듀서 함수를 우선 만들어 주어야 합니다.
const INITIAL_STATE = { name: 'setAge', age: e.currentTarget.value };
const MAX_AGE = 50;
function reducer(state, action) {
switch (action.type) {
case 'setName':
return { ...state, name: action.name };
case 'setAge':
if (action.age > MAX_AGE){
return { ...state, age: MAX_AGE };
} else {
return { ...state, age: action.age };
}
default:
}
}
위에서 만든 리듀서 함수를 가지고 useReducer 훅을 사용하여 상태값을 다루는 코드를 작성해 보겠습니다.
dispatch 함수를 호출하면서 객체를 전달 하면 reducer 의 두번째 인자인 action 으로 전달이 됩니다. 여기서는 항상 type 이라는 값을 전달하여 type 에 따라서 각각의 상태값을 변경하는 로직을 처리하도록 reducer를 구현하였습니다.
이렇게 reducer를 사용하면 상태값을 변경하는 로직을 분리할 수 있기 때문에 이런 점이 장점입니다.
import React, { useReducer } from 'react';
export default function App() {
const [state, dispatch] = useReducer(reducer, INITTIAL_STATE);
return (
<div>
<p>{`name is ${state.name}`}</p>
<p>{`age is ${state.age}`}</p>
<input
type="text"
value={state.name}
onChange{e => dispatch({ type: 'setName', name: e.currentTarget.value })}
/>
<input
type="number"
value={state.age}
onChange{e => dispatch({ type: 'setAge', age: e.currentTarget.value })}
/>
</div>
);
}
보통 리액트로 코딩을 할 때 상위 컴포넌트에서 다수의 상태값을 관리합니다. 이 때 자식 컴포넌트로부터 발생한 이벤트에서 상위 컴포넌트의 상태값을 변경해야 하는 경우가 많습니다. 이를 위해서 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 전달하기도 합니다. 이 작업은 상당히 손이 많이가고, 코드의 가독성도 떨어집니다.
이러한 현상을 해결하기 위해서 아래와 같이 useReducer 훅이랑 Context API 를 같이 이용하면 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 쉽게 전달할 수 있습니다.
ProfileDispatch 컨텍스트를 만든 다음 useReducer 의 상태 변환 함수인 dispatch 함수를 내려주었습니다. 이로써 필요한 컴포넌트에서 dispatch 함수를 이용하여 상태값을 변경할 수 있습니다.
import React, { useReducer } from 'react';
export const ProfileDispatch = React.createContext(null);
export default function App() {
const [state, dispatch] = useReducer(reducer, INITTIAL_STATE);
return (
<div>
<p>{`name is ${state.name}`}</p>
<p>{`age is ${state.age}`}</p>
<ProfileDispatch.Profider value={dispatch}>
<SomeConponent />
</ProfileDispatch.Profider>
</div>
);
}
함수형 컴포넌트에도 멤버 변수나 멤버 함수가 있는 것처럼 사용할 수 있습니다.
Profile 컴포넌트를 보시면, 첫번째 인자인 속성값은 사용하지 않고(_), ref 속성값만 사용하고 있으며, ref 속성값을 받아서 useImperativeHandle 의 첫번째 매개변수로 입력을 하고 있습니다.
Profile 컴포넌트 안에는 name, age 라는 2개의 상태값을 관리하고 있고, useuseImperativeHandle 의 두번째 매개변수로 함수를 입력하고 있는데, 이 함수가 반환한 값이 이 부모의 ref 객체가 참조하는 값이 됩니다.
그리고 이 ref 속성을 받아서 사용하기 위해서 forwardRef 함수를 사용해야 합니다.
import React, { forwardRef, useState, useImperativeHandle } from 'react';
fucntion Profile(_, ref) {
const [name, setName] = userState('horong');
const [age, setAge] = userState(0);
useImperativeHandle(ref, () => ({
addAge: value => setAge(age + value),
getNameLength: () => name.length,
}));
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
</div>
)
}
export default forwardRef(Profile);
정리해 보겠습니다.
1. 부모 객체에서 useRef 훅을 통해서 어떤 ref 객체를 생성했을 것입니다.
2. <Profile ref={생성한 ref 객체} /> 형태로 생성한 ref 객체를 바인딩 해 주었을 것입니다.
3. Profile 컴포넌트에서는 이 객체에 함수를 담아주려고 합니다.
4. useuseImperativeHandle 훅을 사용해서 addAge 함수와 getNameLength 함수를 ref 객체에 담아줍니다.
5. forwardRef 함수를 사용하여 2개의 함수가 잘 담긴 ref 객체를 부모 컴포넌트에 전달해줍니다.
6. 부모 컴포넌트는 useRef 훅을 통해서 선언했던 Profile ref 객체에서 2개의 함수를 자유롭게 사용할 수 있습니다.
실제로 부모 컴포넌트 코드는 아래와 같습니다.
import React, { useRef } from 'react';
import Profile from './Profile';
export default function App() {
const profileRef = useRef();
const onClick = () => {
if (profileRef.current) {
console.log('current name length:', profileRef.current.getNameLength())
profileRef.current.addAge(5);
}
}
return (
<div>
<Profile ref={profileRef} />
<button onClick={onClick}>add age 5</button>
</div>
)
}
지금까지는 useEffect 훅을 많이 사용했습니다. useEffect 함수의 부수 효과 함수는 렌더링 결과가 돔에 반영된 후에 비동기로 호출이 됩니다. useLayoutEffect 훅은 useEffect 훅과 거의 비슷하게 동작하지만 부수 효과 함수가 동기로 호출된다는 차이점이 있습니다. 하지만 타이밍이 미묘하게 다릅니다. useEffect 함수는 실제 돔에 반영된 다음 화면이 그려진 후에 동작을 하지만, useLayoutEffect는 실제 돔에 반영된 다음 화면이 그려지기 전에 동작을 합니다.
그렇기 때문에 useLayoutEffect 훅에 연산을 많이하면 브라우저가 먹통이 될 수도 있습니다. 따라서 특별한 이유가 없다면 useEffect 훅을 사용하는 것이 성능상 이점이 있습니다.
그렇다면, 언제 useLayoutEffect 훅을 사용할까요?
렌더링 직후에 돔 요소의 값을 읽어들이는 경우와 조건에 따라서 컴포넌트를 다시 렌더링 하고 싶은 경우에 사용합니다.
아래의 코드를 예로 들어보겠습니다. 아래의 코드에서 useEffect 는 width 라는 상태값의 최대치를 500 보다 크면 500을 입력해주어서 최댓값을 500으로 고정해주는 코드입니다. 이 코드를 사용하는 경우에 500보다 큰 값이 입력되면 div가 깜박거리면서 500 사이즈에 고정이 됩니다.
이러한 깜박임의 이유는 useEffect 훅이 화면에 그려진 후에 동작하고 있기 때문입니다. 절차를 정리해 보겠습니다.
1. 500 보다 큰 값이 렌더링이 됩니다.
2. useEffect가 실행이 되면서 500으로 다시한번 렌더링을 합니다.
import React, { useState, useEffect } from 'react';
export default function App() {
const [width, setWidth] = useState(200);
useEffect(() => {
if (width > 500) {
setWidth(500);
}
}, [width]);
return (
<div>
<div style={{ width, height: 100, backgroundColor: 'green' }}>test</div>
<button
onClick={() => {
const value = Math.floor(Math.random() * 499 + 1);
setWidth(value);
}}
>
500 이하
</button>
<button
onClick={() => {
const value = Math.floor(Math.random() * 500 + 501);
setWidth(value);
}}
>
500 이상
</button>
</div>
)
}
useLayoutEffect 을 사용하면 화면에 그려지기 전에 동작을 하기 때문에 width 사이즈가 useLayoutEffect 함수에 의해서 500으로 고정된 이후에 화면이 그려지게 됩니다. 따라서 깜박임을 제거할 수 있습니다. 하지만 특별한 경우가 아니라면 useEffect를 사용하는것을 추천합니다.
import React, { useState, useEffect } from 'react';
export default function App() {
const [width, setWidth] = useState(200);
useLayoutEffect(() => {
if (width > 500) {
setWidth(500);
}
}, [width]);
return (
<div>
<div style={{ width, height: 100, backgroundColor: 'green' }}>test</div>
<button
onClick={() => {
const value = Math.floor(Math.random() * 499 + 1);
setWidth(value);
}}
>
500 이하
</button>
<button
onClick={() => {
const value = Math.floor(Math.random() * 500 + 501);
setWidth(value);
}}
>
500 이상
</button>
</div>
)
}
리액트 개발자 도구에 좀 더 풍부한 정보를 제공해줄 수 있습니다.
아래와 같이 useDebugValue 훅을 사용하면, 리액트 개발자 도구의 Components 탭에서 hooks 하위의 ChangeAppState: "?????" 이 물음표 영역에 해당하는 값이 start/running/stop 으로 표시됩니다.
import { useState, useDebugValue } from 'react';
export default function useChangeAppState() {
const [state, setState] = useState(STATE_START);
const next = () => setState(state === STATE_STOP ? STATE_START : state + 1);
useDebugValue(
state === STATE_START
? 'start'
: state === STATE_RUNNING
? 'running'
: 'stop';
);
retturn [state, next];
}
export const STATE_START = 0;
export const STATE_RUNNING = 1;
export const STATE_STOP = 2;
따라서, 실제 코드에서 앱 시작이 연속해서 2회가 보이더라도 디버깅 툴을 이용하여 start - running - stop 상태가 잘 변경된 것이 확인되었다면, 이 부분은 렌더링 할 때 보여질 msg 값이 잘못되었다는 것을 바로 확인할 수 있습니다.
import React from 'react';
import useChangeAppStae, {
STATE_START,
STATE_RUNNING
} from './useChangeAppState';
export default function App() {
const [state, next] = useChangeAppStae(true);
const msg =
state === STATE_START
? '앱 시작'
: state === STATE_RUNNING
? '앱 시작'
: '앱 종료';
retturn (
<div>
<p>{msg}</p>
<button onClick={next}>next</button>
</div>
);
}
출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)
[Frontend/React] 16. prop-types 패키지로 타입 정의하기 (0) | 2020.10.05 |
---|---|
[Frontend/React] 15. 컴포넌트 파일 작성법 (0) | 2020.10.04 |
[Frontend/React] 13. 자식 요소에 접근하기 (0) | 2020.10.03 |
[Frontend/React] 12. Context API 를 사용하여 데이터 전달하기 (0) | 2020.10.03 |
[Frontend/React] 11. 리액트 훅(hook) (0) | 2020.10.03 |