[React.js]리액트 훅 규칙, Context API, 내장 훅
2021. 7. 13. 03:04ㆍWeb_Programming/React
📍 훅 규칙
💡 훅 사용 순서는 늘 같아야 함
👉 if문 for문 등에 넣어 조건적으로 호출되면 안 됩니다.
👉 if 문 return 뒤에 사용도 마찬가지입니다.
💡 함수형 컴포넌트나 커스텀 훅에서만 호출 가능
📍 Context API
속성 값을 하위 컴포넌트로 넘겨 사용할 경우,
컴포넌트 트리가 깊다면 속성값을 여러 컴포넌트에 걸쳐 넘겨줘야 하는 불편함이 있습니다.
이를 해결할 수 있는 방법으로 context API가 있습니다.
import React, {useContext, createContext } from 'react';
const UserContext = createContext('unknown');
export default function App() {
return (
<div>
<UserContext.Provider value="horong">
<div>상단 메뉴</div>
<Profile />
<div>하단 메뉴</div>
</UserContext.Provider>
</div>
);
}
function Profile() {
return (
<Greeting />
{/* ... */}
);
}
/*
function Greeting() {
return (
<UserContext.Consumer>
{username => <p>{`${username}님 안녕하세요`}</p>}
</UserContext.Consumer>
)
}
*/
function Greeting() {
const username=useContext(UesrContext);
return (<p>{`${username}님 안녕하세요`}</p>)
}
💡 Provider를 이용하여 값 설정
💡 Consumer 를 이용하여 값 사용
👉 가장 가까운 상위 Provider 컴포넌트를 찾아 값을 받는다.
👉 못 찾으면 초기 설정 값으로 사용.
❗ Provider로 값 변경 시 Cousumer까지의 컴포넌트 리 랜더링
+ 중간 컴포넌트 리랜던링 안 해도 잘적용
useContext훅으로 사용 가능
💡 속성 값 변경 함수
속성값 변경 함수 또한 Context로 넘겨줄 수 있습니다.
상위 컴포넌트
import React, { createContext } from 'react';
const UserContext = createContext({ username: 'unknown', helloCount: 0 });
const SetUserContext = createContext(() => {});
export default function App() {
const [user, setUser] = useState({ username: 'horong', helloCount: 0 });
return (
<div>
<SetUserContext.Provider value={setUser}>
<UserContext.Provider value={user}>
<div>상단 메뉴</div>
<Profile username="horong" />
<div>하단 메뉴</div>
</UserContext.Provider>
<SetUserContext.Provider>
</div>
);
}
하위 컴포넌트
import React, { useContext, createContext } from 'react';
export default function Greeting() {
const setUser = useContext(SetUserContext);
const { username, helloCount } = useContext(UserContext);
return (
<>
<p>{`${username}님 안녕하세요`}</p>
<p>{`인사 횟수: ${helloCount}`}</p>
<button onClick={() => setUser({ username, helloCount: helloCount + 1})}>
인사하기
</button>
</>
);
}
❗ Provider는 매번 랜더링 될 때마다 새로운 객체가 생기며,
Consumer의 컴포넌트가 사용하지 않는 속성 값의 변경으로도 리 랜더링 되는 문제가 발생
👉 해결 방법, 새로운 객체가 매번 생기지 않게 하나의 훅으로 관리
import React, { createContext } from 'react';
const UserContext = createContext({ username: 'unknown', age: 0 });
export default function App() {
/*
const [username, setUsername] = useState('');
const [age, setAge] = useState(0);
*/
const [user, setuser] = useState({ username: 'horong', age: 23 });
return (
<div>
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
</div>
);
}
+ Provider 안에서 consumer를 사용해야 Provider에 도달하여 값을 받을 수 있습니다.
📍 useRef
ref속성 값을 이용하여 돔 요소에 접근
👉 이를 리액트에서는 useRef 훅으로 접근 가능하게 해 줍니다.
import React, { useRef, useEffect } from 'react';
export default function App() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, [])
return (
<div>
<input type="text" ref={inputRef}/>
<button>저장</button>
</div>
)
}
❗ ref는 랜더링 할 때마다 새로운 함수를 호출합니다
👉 사용자의 입력값이 매번 초기화되는 문제
import React, { useState } from 'react';
export default function App() {
const [text, setText] = useState(INITIAL_TEXT);
const [showText, setShowText] = useState(true);
return (
<div>
{showText && (
<input type="text"
ref={ref => ref && setText(INITIAL_TEXT)}
value={text}
onChange={e => setText(e.target.value)}
/>
)}
<button onClick={() => setShowText(!showText)} >보이기/가리기</button>
</div>
)
}
const INITIAL_TEXT = '안녕하세요';
💡 이를 해결할 수 있는 방법으로 useCallback을 사용합니다.
useCallback 훅의 메모 이제이 션 덕분에 한번 생성된 함수를 재사용할 수 있습니다.
따라서 ref 속성이 계속 새로운 함수로 인식하지 않도록 도와줍니다.
import React, { useState } from 'react';
export default function App() {
const [text, setText] = useState(INITIAL_TEXT);
const [showText, setShowText] = useState(true);
const setInitialText = useCallback(ref => ref && setText(INITIAL_TEXT));
return (
<div>
{showText && (
<input type="text"
ref={setInitialText}
value={text}
onChange={e => setText(e.target.value)}
/>
)}
<button onClick={() => setShowText(!showText)} >보이기/가리기</button>
</div>
)
}
const INITIAL_TEXT = '안녕하세요';
💡 돔을 여러 개 사용할 때
useRef를 여러개 매번 사용할 때 문제가 발생할 수 있습니다.
이때는 함수를 이용하여 ref를 관리합니다.
여러 개의 돔들을 한 번에 관리할 수 있는 boxListRef 객체를 만들고, 오브젝트로 초기화를 합니다.
그 후, BOX_LIST 데이터를 렌더링을 해주면서 동시에 각각의 div 참조 객체를 boxListRef에 넣어서 관리합니다.
import React, { useContext, createContext, useState, useRef } from 'react';
export default function App() {
const boxListRef = useRef({});
return (
<div>
<div
style={{
display: 'flex',
flexWrap:'wrap',
width: '100%',
height: '100vh',
}}
>
{BOX_LIST.map(item => (
<div
key={item.id}
ref={ref => (boxListRef.current[item.id] = ref)}
style={{
flex: '0 0 auto',
width: item:width,
height: 100,
backgroundColor: 'yellow',
border: 'solid 1px red',
}}
>{`box+${item.id}`}</div>
))}
</div>
</div>
);
}
const BOX_LIST = [
{ id: 1, width: 50 },
{ id: 2, width: 30 },
{ id: 3, width: 10 },
{ id: 4, width: 50 },
{ id: 5, width: 70 },
{ id: 6, width: 30 },
{ id: 7, width: 20 }
];
📍 리액트 내장 훅
💡 useRef
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);
}
});
}
👉 렌더링과 무관한 데이터들은 useRef를 사용하여 다루어 주는 것이 좋습니다.
💡useMemo
useMemo는 계산량이 많은 함수의 반환 값을 재활용하는 용도로 사용됩니다.
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 안에 첫 번째 매개변수 : 복잡한 계산을 하는 함수
👉 리액트가 함수의 반환 값을 기억
useMemo 안에 두 번째 매개변수 : 이 함수는 v1, v2 값이 하나라도 변경되면 실행
두 번째 인자 배열 값(useEffect와 마찬가지로 의존성 배열)이 변경되지 않았다면
👉 이전에 실행된 함수의 결괏값을 재활용
💡 useCallback
랜더링으로 새로운 함수가 만들어져서 전달되면, 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 훅을 사용하여 name, age 값이 변경될 때만 새로운 함수가 생성되도록 작성을 했습니다.
+useCallback도 의존성 배열로 관리
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
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:
}
}
👉 dispatch 함수를 호출하여 상태 값을 변경
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>
);
}
👉 이렇게 reducer를 사용하면 상태값을 변경하는 로직을 분리할 수 있다는 것이 장점
useReducer 훅이랑 Context API를 같이 이용
👉 자식 컴포넌트로부터 발생한 이벤트에서 상위 컴포넌트의 상태 값을 변경해야 하는 경우
👉 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 쉽게 전달할 수 있습니다.
콘텍스트 provider에 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.Provider value={dispatch}>
<SomeConponent />
</ProfileDispatch.Provider>
</div>
);
}
💡 useImperativeHandle
함수형 컴포넌트에도 Class처럼 멤버 변수나 멤버 함수가 있는 것처럼 사용할 수 있습니다.
- ref 속성 값을 받아 useImperativeHandle의 첫 번째 매개변수로 입력
- 두 번째 매개변수로 함수를 입력하고 있는데, 이 함수가 반환한 값이 이 부모의 ref 객체가 참조하는 값
❗ ref 속성을 받아서 사용하기 위해서 forwardRef 함수를 사용해야 합니다.
👉 부모 컴포넌트는 useRef 훅을 통해서 선언했던 ref 객체에서 2개의 함수를 자유롭게 사용할 수 있습니다.
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);
상위 컴포넌트
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>
)
}
💡 useLayoutEffect
useLayoutEffect 훅은 useEffect 훅과 거의 비슷하게 동작하지만
부수 효과 함수가 동기로 호출된다는 차이점이 있습니다.
- useEffect 함수는 실제 돔에 반영된 다음 화면이 그려진 후에 동작
- useLayoutEffect는 실제 돔에 반영된 다음 화면이 그려지기 전에 동작
❗ useLayoutEffect 훅에 연산을 많이 하면 브라우저에 무리가 가므로, useEffect 훅을 사용하는 것이 성능상 이점이 있습니다.
👉 렌더링 직후에 돔 요소의 값을 읽어 들이는 경우
👉 조건에 따라서 컴포넌트를 다시 렌더링 하고 싶은 경우에 사용
'Web_Programming > React' 카테고리의 다른 글
[React.js] 리액트의 useEffect 활용법 & 성능 최적화 방법 (0) | 2021.07.19 |
---|---|
[React.js]리액트 타입선언, 조건부 렌더링, 컴포넌트 재사용성 (0) | 2021.07.16 |
[React.js]리액트의 가상돔, 리액트 훅 기초 (0) | 2021.07.11 |
[React.js]리액트의 상탯값, 속성값, 반환값 (0) | 2021.07.08 |
[React]구글 Oauth2 로그인 구현 with TypeScript (0) | 2021.06.09 |