useCallback, React.memo, useMemo는 잘 사용하면 리렌더링을 줄이는데 유용
useCallback, React.memo, useMemo 모두 렌더 페이즈에 관여하여 불필요한 리렌더링이 발생하지 않도록 하는 것이 주요 목적
- useMemo와 useCallback의 차이:
- useMemo: 메모이제이션된 값을 return하는 훅
- useCallback: 메모이제이션된 함수를 반환하는 훅
ㅤ- useMemo와 React.memo의 차이:
- useMemo:
- hook이라서 함수형 컴포넌트에서만 사용 가능
- 리턴되는 값을 메모이제이션 하는데, 의존성 배열에 있는 값이 바뀌는 경우 호출
- React.memo:
- 클래스형 컴포넌트와 함수형 컴포넌트에서 사용 가능
- 컴포넌트를 메모이제이션하는데, 컴포넌트의 props가 바뀌지 않으면 리렌더링 하지 않음
React 18부터 도입된 새로운 루트 생성방법. 비동기 랜더링과 병령모드를 지원한다.
const [count, setCount]=useState()
기본적인 해부도
useState처럼 state를 생성하고 관리할 수 있다.
{
teacher: 'James',
students: ['Kim', 'Ann', 'Jhon'],
count: 3,
locations: [
{country: 'Korea', name: 'A'},
{country: 'Australia', name: 'B'}
]
}
```
상황예시:
철수가 은행에서 만원을 출금해달라고 요구한다. 그래서 철수의 계좌에 거래내역(state)이 남는다.
여기서 중요한 것은 철수는 자신의 내역을 직접 업데이트 하지 않는다. 철수 대신 은행이 한다.
철수는 요구라는 Dispatch
안에 내용이라는 Action
을 담아 은행(Reducer
)에게 전달해서 sate
를 변경할 수 있다.
예시 코드의 기능
사용자는 숫자를 입력하고 "예금" 버튼을 클릭하면 입력한 금액이 잔고에 추가되며, "출금" 버튼을 클릭하면 입력한 금액이 잔고에서 차감한다.
해당 코드에서 useReducer 사용 목적
사용자의 예금 또는 출금 요청에 따라 상태를 적절히 변경하는 기능을 수행한다.
//state에는 money의 값이 들어감
const reducer = (state, action) => {
//예금 버튼과 출금 버튼의 state 관리 기능이 달라야 한다.
//보통 if문이나 switch문을 많이 쓴다.
switch (action.type) {
case 'deposit':
return state + action.payload
case 'withdraw':
return state - action.payload
default:
return state
}
}
function App() {
/*useState는 은행의 잔고의 상태를 관리한다.
예금,출금에 따라 다르게 관리해야 하므로useReducer에 의해 관리당하고 있다.*/
const [number, setNumber] = useState(0)
const [money, dispatch] = useReducer(reducer, 0) //0: money의 초기값
return (
<div>
<h2>useReducer 은행에 오신 것을 환영합니다다</h2>
<p>잔고: {money}원<p>
<input
type='number'
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
step='1000'
/>
<button onClick=(() => {
//action에 전달된다
dispatch({type: 'deposit', payload: number})
})>예금</button>
<button onClick=(() => {
//action에 전달된다
dispatch({type: 'withdraw', payload: number})
})>출금</button>
</div>
)
}
useReducer
는 두 번째 인자로 초기 상태를 받는다.money
의 초기값이 0
으로 설정되어 있다.Reducer
함수는 현재 state
와 action
을 받아 새로운 state
를 반환한다.dispatch
함수는 action
객체를 인수로 받아 Reducer 함수에 전달한다.state
를 업데이트하는 유일한 방법이다.이 코드는 출석부 애플리케이션으로, 사용자가 학생 이름을 입력하면 해당 학생이 목록에 추가되고 총 학생 수가 업데이트 된다.
코드가 복잡해보이지 않게 학생 추가 기능만 구현했다.
// 기본값 설정: 초기 상태를 정의
const initialState = {
count: 0, // 학생 수를 나타내는 상태
students: [] // 학생 목록을 나타내는 상태
}
// reducer 함수: 상태 변화 로직을 관리
const reducer = (state, action) => {
switch (action.type) {
// 'add-student' 액션이 발생했을 때의 상태 변화 처리
case 'add-student':
const name = action.payload.name; // 액션에서 전달된 학생 이름을 가져온다.
const newStudent = {
id: Date.now(),
name,
isHere: false // 초기 출석 상태를 false로 설정
}
return {
count: state.count + 1, // 학생 수를 1 증가
students: [...state.students, newStudent] // 기존 학생 목록에 새 학생을 추가
}
default:
return state; // 액션 타입이 일치하지 않으면 기존 상태를 그대로 반환
}
}
function App() {
const [name, setName] = useState(''); // 입력된 학생 이름을 관리하는 state
const [studentsInfo, dispatch] = useReducer(reducer, initialState); // 학생 정보를 관리하는 state와 dispatch 함수
return (
<div>
<h2>출석부</h2>
{/* 현재 학생 수를 출력 */}
<p>총 학생 수 {studentsInfo.count}명</p>
{/* 학생 이름을 입력받는 input 필드 */}
<input
type='text'
placeholder='이름을 입력해주세요.'
value={name}
onChange={(e) => setName(e.target.value)}
/>
{/* '추가' 버튼을 클릭하면 'add-student' 액션을 dispatch한다. */}
<button onClick={() => {
dispatch({type: 'add-student', payload: {name}});
setName(''); // 추가 후 입력 필드를 초기화
}}>추가</button>
{/* 학생 목록을 출력 */}
<ul>
{studentsInfo.students.map(student => (
<li key={student.id}>{student.name}</li>
))}
</ul>
</div>
)
}
Context API: 앱 안에서 전역적으로 사용되는 데이터들을 여러 컴포넌트들끼리 쉽게 공유할 수 있는 방법을 제공
useContext: Context API를 사용하는 방법 중 하나, Context로 공유한 데이터를 쉽게 받아올 수 있게 도와주는 역할
Context API의 목적: 다양한 레벨에 있는 컴포넌트들에게 props를 통하지 않고 전역적으로 데이터를 전달하기 위함
Context는 꼭 필요할 때만!
- Context를 사용하면 컴포넌트를 재사용하기 어려워질 수 있다.
- Props drilling을 피하기 위한 목적이라면 컴포넌트 합성을 먼저 고려해보자.
사용 예시
간단한 테마(Context)를 만들어서 버튼의 배경색을 변경
import React, { createContext, useContext, useState } from 'react';
// 기본 테마 값
const ThemeContext = createContext();
const App = () => {
// 테마 상태를 관리 (light 또는 dark)
const [theme, setTheme] = useState('light');
// 테마 토글 함수
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
};
const Toolbar = () => {
return (
<div>
<ThemeButton />
</div>
);
};
const ThemeButton = () => {
// useContext로 ThemeContext에서 데이터를 가져옴
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '10px',
border: 'none',
borderRadius: '5px',
}}
onClick={toggleTheme}
>
{theme === 'light' ? 'Switch to Dark Theme' : 'Switch to Light Theme'}
</button>
);
};
export default App;
기본 사용법
import React, { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 이 코드는 컴포넌트가 마운트될 때 실행
console.log('컴포넌트가 마운트되었습니다.');
// 클린업 함수
return () => {
// 이 코드는 컴포넌트가 언마운트될 때 실행
console.log('컴포넌트가 언마운트되었습니다.');
};
}, []); // 빈 배열을 넣으면 마운트와 언마운트 때만 실행
return <div>안녕하세요!</div>;
}
의존성 배열
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// count 값이 변경될 때마다 실행됩
console.log(`count가 ${count}로 변경되었습니다.`);
}, [count]); // count가 의존성 배열에 포함되어 있다.
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
의존성 배열이 없는 경우
의존성 배열을 생략하면 컴포넌트가 렌더링될 때마다 useEffect가 실행
import React, { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
console.log('컴포넌트가 렌더링되었습니다.');
}); // 의존성 배열이 없음
return <div>안녕하세요!</div>;
}
요약
useEffect 함수 내에서 비동기 작업을 수행할 때 async/await를 직접 사용하는 것은 허용되지 않는다.
.then체이닝을 사용한다.
혹은 코드를 async/await로 바꾸고 싶다면, 비동기 함수를 선언하고 그 함수를 useEffect에서 호출하는 방식을 사용한다.
이렇게 useEffect를 활용하면 컴포넌트가 라이프사이클에 맞춰 올바르게 동작하도록 관리할 수 있다.
useEffect 의존성 배열 관리 방법
의존성 배열은 잘못 관리하면 쉽게 버그로 이어지기 때문에 입력하지 않는게 좋지만 필요에 있어 입력할 일이 생긴다.
경우에 따라서는useCallback
,useReducer
을 활용할 수 있다.
의존성 배열에는 무엇을 넣어야 하는가?
의존성 배열에 무엇을 넣어야 하는지 헷갈릴 수 있다. 특히 상태 업데이트 함수인
useState
의setState
와 같은 것들이 의존성 배열에 포함되어야 하는지에 대해 궁금증을 가질 수 있다.
이를 쉽게 이해할 수 있도록 간단한 예시와 함께 알아보자.
예시 코드import React, { useState, useEffect } from "react"; function ExampleComponent() { ㅤ const [count, setCount] = useState(0); const [message, setMessage] = useState(""); ㅤ useEffect(() => { console.log("Count가 변경되었습니다:", count); setMessage(`Count는 ${count} 입니다.`); }, [count]); ㅤ return ( <div> <p>{message}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } ㅤ export default ExampleComponent;
위 코드에서 useEffect 훅은 count 값이 변경될 때마다 실행된다. useEffect 내부에서 count 값에 따라 메시지를 설정하는 setMessage 함수가 호출된다.
왜 setMessage는 의존성 배열에 포함되지 않는가?
이유는 리액트에서 setState 함수(이 경우 setMessage)는 불변이며, 컴포넌트가 마운트될 때 한 번 정의된 후 변경되지 않는다. 따라서 setMessage는 언제나 같은 함수를 참조하므로, 의존성 배열에 포함할 필요가 없다.
의존성 배열에 포함해야 하는 것
- 외부에서 정의되고 변경될 수 있는 값: 이 값들이 변경되면 useEffect가 다시 실행되어야 한다.
- 상태 변수, props, 또는 다른 함수/변수: 컴포넌트 외부에서 정의되거나, 재생성될 가능성이 있는 변수나 함수들을 의미한다.
의존성 배열에 포함하지 않아도 되는 것
- 상태 업데이트 함수 (setState 함수): 이 함수들은 리액트에서 보장된 불변 참조를 가지므로, 의존성 배열에 포함할 필요가 없다.
최종 정리
useEffect 훅의 의존성 배열에는 변경될 수 있는 값들만 포함시키면 된다.
상태 업데이트 함수는 변경되지 않으므로, 배열에 포함하지 않아도 된다. 이를 잘 이해하고 사용하면, 리액트에서 불필요한 리렌더링을 방지하고, 성능을 최적화할 수 있다.
DOM 변경이 일어난 직후에 동기적으로 실행되는 훅
useLayoutEffect
와 useEffect
는 둘 다 리액트에서 사이드 이펙트를 처리하기 위해 사용되는 훅이며, 특정 조건이 변경되었을 때 어떤 작업을 수행하는 데 사용된다는 공통점이 있다.
useEffect와의 공통점
useEffect와의 차이점
해당 차이점으로 인해 사용자에게 보여지는 ui의 변화를 더 정교하게 다룰 수 있다.
예시:
컴포넌트가 화면에 보여지기 전에 effect 안에서 어디에 어떻게 보여줄 것인지 미리 하고 나서 업데이트 된 화면을 사용자에게 보여줄 수 있다.
컴포넌트를 화면에 배치하기 전에 레이아웃이나 스크롤 같은 ui적인 계산을 해야할 때
예시
다음 코드의 목적은 해당 컴포넌트가 렌더링 될 때 스크롤을 맨 아래로 내린 상태로 렌더링 하는 것이다. 하지만 브라우저의 성능이 저하됐을 때 약간의 딜레이 때문에 스크롤이 처음에 맨 위에 있다가 내려가는 문제가 있다.
이런 경우 useLayoutEffect을 이용해 해결할 수 있다.
function getNumbers() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
}
function App() {
const [numbers, setNumbers] = useState([]);
const ref = useRef(null);
useEffect(() => {
// 컴포넌트가 처음 렌더링될 때 숫자 목록을 가져와 상태에 저장
const nums = getNumbers();
setNumbers(nums);
}, []);
useLayoutEffect(() => {
// 숫자 목록이 업데이트된 후 스크롤을 맨 아래로 내림
if (numbers.length === 0) {
return;
}
// 스크롤 위치를 가장 아래로 설정
ref.current.scrollTop = ref.current.scrollHeight;
}, [numbers]); // numbers가 변경될 때마다 실행
return (
<div
ref={ref}
style={{
height: "300px",
border: "1px solid blue",
overflow: "scroll",
}}
>
{/* 숫자 목록을 화면에 표시 */}
{numbers.map((number, idx) => (
<p key={idx}>{number}</p>
))}
</div>
);
}
메모이제이션 기법으로 컴포넌트의 성능을 최적화
useMemo
는 리턴값을 메모이제이션 해주지만 useCallback
은 콜백함수 그 자체를 메모이제이션
예시
//함수가 필요할 때마다 새로 생성하지 않고 메모리에서 가져와서 재사용
const calculate = useCallback((num) => {
return num + 1
}, [item])
구성
함수형 컴포넌트가 함수라는 것의 의미
컴포넌트 렌더링 -> 컴포넌트 함수 호출 -> 모든 내부 변수 초기화
useCallback
을 사용하여 해당 컴포넌트가 처음 렌더링 될 때만 함수 객체를 초기화const [number, setNumber] = useState(0)
//의존성 배열에 number를 추가하지 않으면 setNumber가 올라가도 someFunction 콘솔의 number는 0이다.
const someFunction = useCallback(()=>{
consle.log(`someFunc: number: ${number}`)
}, [])
//객체의 참조에 변화가 없으므로 useEffect가 실행되지 않는다.
useEffect(()=>{
console.log('someFunction이 변경되었습니다.')
}, [someFunction])
return (
<div>
<input
type='number'
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<button onClick={someFunction} >Call someFunc</button>
)
메모이제이션이란?
객체를 캐시하여 불필요한 재생성을 방지
어떠한 자주 사용되는 값을 받아오기 위해 받아오기 위해 반복적으로 계산을 하지 않고 이미 계산한 값을 캐싱해두고 메모리에서 꺼내서 재사용하는 기법
이 메서드는 오직 성능 최적화를 위하여 사용된다. 렌더링을 “방지”하기 위하여 사용하면 안 된다. 버그를 만들 수 있다.
[]
안의 값이 업데이트 될 때만 콜백함수를 다시 호출해서 메모이제이션된 값을 엄데이트해서 다시 메모이제이션 해준다.const asdf = React.memo(Btn)
asdf은 메모라이즈 버전의 Btn이 될 것이다.
만약 부모가 어떤 state라도 변경이 있다면 모든 자식들(여기선 Btn들)은 다시 그려질 것이다.
함수형 컴포넌트는 함수이고, 함수형 컴포넌트가 렌더링이 된다는 건 그 함수가 호출되는 것이고, 함수는 호출되면 내부의 모든 변수들이 초기화된다.
이것은 앱이 느려지는 원인이 될 수 있다.
그러므로 React.memo()로 Props 변경되지 않은 Btn은 다시 그려질 필요가 없다고 말한 것이다.
동일한 값을 리턴하는 함수를 반복적으로 호출해야 한다면 맨 처음 값을 계산할 때 해당 값을 메모리에 저장해서 필요할 때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용하는 기법
React.memo는 props 변화에만!!! 영향을 준다.
React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링된다.function DragabbleCard({ toDoId, text, index }: DragabbleCardProps) { return ( ... ); } export default React.memo(DragabbleCard);
객체를 캐시하여 불필요한 재생성을 방지(메모이제이션)
사실 1초 이상 걸릴 함수로 컴포넌트 내부에 변수를 초기화해줄 상황은 적다.useMemo()
가 빛을 발하는 상황은 따로 있다.const tokenData = { key: value; } const router = useRouter(); useEffect(() => { if (tokenData) { router.push("/"); } }, [tokenData, router]);
이 코드는 tokenData가 변화할 때 페이지를 이동하는 것이다. 하지만 여기서 만약 tokenData가
원시타입
이 아닌객체타입(원시타입을 제외한 모든 것)
이라면 변수에 메모리 주소가 할당되므로 App 컴포넌트가 다시 호출될 때 이전 tokenData 객체와는 다른 메모리상 공간에 저장이 된다. 그리고 tokenData은 또 생성된 객체의 주소를 참조하게 된다. 즉, 리액트의 관점에선 tokenData 안에 있는 변수의 주소가 바뀌었으므로 tokenData는 변경이 됐다고 인식한다.
원시타입과 객체타입
원시타입과 객체타입 알아보기const tokenData = useMemo(() => { return { key: value; } } const router = useRouter(); useEffect(() => { if (tokenData) { router.push("/"); } }, [tokenData, router]);
그러므로 객체타입의 변수를 이용할 땐 이렇게 사용하면 되겠다.
참고
별코딩 - useMemo 제대로 사용하기 영상
사용예시
DND에서 카드를 옮길 때 떨리는 현상 해결
혹시 React.memo 가 안되면 function에 export가 있는 지 확인해보자.
memo 사용할 때는 function에서 export 하면 안되고 파일 자체에서 export 해야한다.
123
const x = useParams()
useParams()가 x에 id를 부여해 주었다. {id: '53528'}
파라미터 값을 URL을 통해서 넘겨받은 페이지에서 사용할 수 있도록 해줌.
예를 들어, 특정 제품 리스트에서 제품을 클릭 시 제품의 세부 정보를 나타내는 페이지로 이동하고 싶다면
제품의 id 값을 URL로 넘겨 세부 페이지에서 id 값에 해당하는 제품만 보여줄 수 있다.
React Router v6에서 제공하는 훅으로, URL의 쿼리 스트링(검색 매개변수)을 읽고 설정할 수 있게 해준다.
?name=kim&age=12
import { useSearchParams } from "react-router-dom";
const [searchParams, setSearchParams] = useSearchParams();
//.get() 이외에도 많은 유틸리티 메소드를 제공
const name = searchParams.get('name');
const age = searchParams.get('age');
const updateQueryString = () => {
//url에 해당 키와 값을 가지는 쿼리스트링 추가
setSearchParams({ name: 'John', age: 30 });
};
return (
<div>
<p>이름: {name}</p>
<p>나이: {age}</p>
<button onClick={updateQueryString}>쿼리 스트링 업데이트</button>
</div>
);
}
//혹은
const [readSerachParams, setReadSearchParams] = useSearchParams()
setTimeout(() => {
setReadSearchParams({
day: 'today',
tommorrow: '123'
})
}, 3000);
자주 사용하는 메서드
값을 읽어오는 메서드
searchParams.get(key)
- 특정한 key의 value를 가져오는 메서드, 해당 key 의 value 가 두개라면 제일 먼저 나온 value 만 리턴
searchParams.getAll(key)
- 특정 key 에 해당하는 모든 value 를 가져오는 메서드
searchParams.toString()
- 쿼리 스트링을 string 형태로 리턴값을 변경하는 메서드
searchParams.set(key, value)
- 인자로 전달한 key 값을 value 로 설정, 기존에 값이 존재했다면 그 값은 삭제됨
searchParams.append(key, value)
- 기존 값을 변경하거나 삭제하지 않고 추가하는 방식
serchParams 을 변경하는 메서드로 값을 변경해도 실제 url 의 쿼리 스트링은 변경되지 않는다. 이를 변경하려면 setSearchParams 에 searchParams 를 인자로 전달해야 한다.
기존 쿼리스트링을 유지하면서 새로운 값을 추가
const [searchParams, setSearchParams] = useSearchParams(); ㅤ const onTheaterClick = (theaterId: number) => { // 기존의 searchParams 복사 const updatedParams = new URLSearchParams(searchParams); // 새로운 theaterId를 추가 updatedParams.set("theaterId", String(theaterId)); // searchParams 업데이트 setSearchParams(updatedParams); };
이렇게 하면 기존 쿼리스트링을 유지하면서 새로운 theaterId 값을 추가하거나, 이미 같은 키가 존재하면 그 값을 업데이트하게 된다.
여러 개의 theaterId를 계속 추가하고 싶을 경우
const [searchParams, setSearchParams] = useSearchParams(); ㅤ const onTheaterClick = (theaterId: number) => { // 기존의 searchParams 복사 const updatedParams = new URLSearchParams(searchParams); // 새로운 theaterId를 추가 (기존 값 유지) updatedParams.append("theaterId", String(theaterId)); // searchParams 업데이트 setSearchParams(updatedParams); };
theaterId가 여러 번 클릭될 때마다 동일한 키로 여러 값을 가지는 형태로 쿼리스트링에 추가
- 쿼리스트링은 검색, 필터링, 페이지네이션 등에 다양하게 활용
- state 는 페이지네이션으로 이동할 때 초기화되지만 쿼리 스트링은 url 에 정보가 담겨 있어서 해당 페이지를 그대로 유지한다. 필터링, 검색 결과 등 해당 정보가 지속적으로 유지되어야 하는 경우에 쿼리 스트링을 활용하는 것이 좋다.
리액트 라우터 v6부터, 이전 버전에선 useHistory가 유사한 기능을 수행
프로그래밍 방식으로 페이지를 이동
const navigate = useNavigate();
const handleButtonClick = () => {
navigate('/contact');
// navigate('/contact', {replace: true});
};
<button onClick={() => { navigate(-1); }} >//string 타입이 아니라 number 타입임
//이전 페이지로 이동하기(양수는 다음 페이지)
</button>
const location = useLocation()
사용자가 현재 머물러있는 페이지에 대한 정보를 알려준다.
객체 형식으로 보여주기 때문에 location.state로 접근할 수 있다.
params처럼 API에서 정보를 받아오는 것이 아닌, 이전 페이지에서 정보를 가져온다.
useNavigate()와 useLocation()으로 데이터 주고받기
리액트 라우터 v6으로 업데이트 되면서 삭제
const priceMatch = useRouteMatch(`/${coinId}/price`);
리액트마스터#5.8참고
현재 브라우저가 방문중인 url이 내가 인자()
로 넣은 url(여기선 /${coinId}/price
) 패턴과일치하는지 확인할수 있는 react-router-dom hook.
주로 조건부 랜더링에 이용된다.
사용자가 어떤 URL에 있는지의 여부를 알려준다.
해당 url을 선택하고 콘솔로그를 보면 어떤 오브젝트가 뜨고 isExact가 true를 반환하며, 선택하지 않으면 null이 출력된다.
useRouteMatch
와 사용법은 동일하다.
const match = useMatch(“/:coinId/price”);
console.log(match)
//현재 url 상태가 매개변수에 들어간 url과 같다면 true를 반환, 다르면 false를 반환한다.
주의점
const { nickname } = useParams(); const mostcyallMatch = useMatch(`/${nickname}/mostcyall`); //위처럼 하지 말고 아래처럼 해야함 const { nickname } = useParams(); const mostcyallMatch = useMatch("/:nickname/mostcyall");
useRef<HTMLDivElement>(null)
ts로 div를 조작하는 경우
render 메서드에서 생성된 노드 혹은 React Element에 접근하는 방법
current 객체의 메서드들은 기본적으로 바닐라 자바스크립트에서 DOM 요소를 조작할 때 사용할 수 있는 메서드들이다.
Valilla Javascript에서는 DOM 객체에 접근하기 위해 querySelector나 getElementById API를 사용해야 하지만, React에서는 DOM API를 중구난방 사용할 경우 디버깅, 코드 해석(리뷰)이 복잡해진다.
그런 단점을 해소하고자 DOM 제어를 ref 라는 속성 기능이 대신 수행 해준다.
https://velog.io/@milkyway/useRef-%EC%82%AC%EC%9A%A9%EB%B2%95
또한 저장 공간으로도 자주 활용하기도 한다.
사용법은 2 가지가 있다.
사용법1: 컴포넌트가 유지되는 동안에 변하지 않았으면 하는 값을 저장
state와의 차이점
1. State가 변할 경우에는 컴포넌트가 렌더링되면서 컴포넌트 내부 변수들이 모두 초기화된다.
2. 하지만 Ref안의 값을 바꿔도 리액트는 컴포넌트를 다시 렌더링하지 않는다.
즉, 해당 Ref의 값은 화면이 다시 그려지지 않지만 유지가 된다는 뜻
useState를 통해 관리하는 state는 해당 상태가 변하면 화면을 다시 그리고(렌더링), useRef는 그렇지 않다는 것을 잘 기억하자.
function App() {
const countRef = useRef(0);
const upCountRef = () => {
countRef.current = countRef.current + 1;
console.log("Ref >> ", countRef.current);
};
return (
<>
<p>Ref: {countRef.current}</p>
<button onClick={upCountRef}>Ref UP</button>
</>
);
}
export default App;
여기서 버튼을 눌러도 화면상의 값은 업데이트되지 않지만 콘솔에선 숫자가 올라간다.
사용법2: Ref를 통해 DOM에 접근
function App() {
const inputElement = useRef();
const focusInput = () => {
inputElement.current.focus();
};
return (
<>
<input type="text" ref={inputElement} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
export default App;
이렇게 작성하면 아래와 같이 뜬다.
Focus Input 버튼
을 누르면 input 태그로 focus 된다.
코드의 흐름을 보면 리액트에서 제공하는 ref라는 변수가 있다.
<input type="text" ref={inputElement} />
ref의 값에 useRef()를 받은 변수를 넣으면, 그 변수를 통해 해당 요소를 제어할 수 있다.
지금 우리가 제어할 수 있는 요소는 input 태그다.
input 태그는 HTMLElement이기 때문에 focus 메서드를 지원한다.
순서요약
1. 내가 접근하고 싶은 태그에 ref라는 변수를 선언하고
2. 거기에 useRef를 받아온 변수를 넣는다.
2. useRef를 받아온 변수.current.내가사용하고싶은메서드 <-- 이렇게 사용해서 해당 Element를 컨트롤한다.
useRef와 scrollIntoView()를 이용해
채팅창
을 맨 밑으로 유지하는 방법// useRef로 스크롤할 DOM을 선택하고 useEffect와 scrollIntoView로 스크롤 const scrollRef = useRef<HTMLDivElement>(null); useEffect(() => { scrollRef?.current?.scrollIntoView(); }); ... // 메세지들 목록 맨 밑에 빈 div를 만들어 ref를 설정 {data?.stream.messages.map...} <div ref={scrollRef}/>
current 객체의 주요 메소드
current 객체의 메서드들은 기본적으로 바닐라 자바스크립트에서 DOM 요소를 조작할 때 사용할 수 있는 메서드들이다.
따라서 current 속성으로 참조된 DOM 요소는 바닐라 자바스크립트에서 사용 가능한 모든 메서드와 속성을 사용할 수 있다.
- click()
- 해당 요소를 클릭한 것처럼 동작한다. 버튼이나 링크와 같은 요소에서 사용된다.
- focus()
- 해당 요소에 포커스를 준다. 주로
<input>
,<textarea>
와 같은 폼 요소에서 사용된다.- 예시:
inputRef.current.focus();
- blur()
- 해당 요소에서 포커스를 제거한다.
- 예시:
inputRef.current.blur();
- scrollIntoView()
- 해당 요소가 보이도록 페이지를 스크롤한다. 선택적으로 스크롤 애니메이션이나 스크롤 위치를 지정할 수 있다.
예시:elementRef.current.scrollIntoView({ behavior: 'smooth' });
- select()
- 입력 요소에서 텍스트를 선택한다. 주로
<input>
요소에서 사용된다.- 예시:
inputRef.current.select();
- setAttribute(name, value)
- 해당 요소에 새로운 속성을 추가하거나 기존 속성의 값을 설정한다.
- 예시:
elementRef.current.setAttribute('aria-hidden', 'true');
- removeAttribute(name)
- 해당 요소의 속성을 제거한다.
- 예시:
elementRef.current.removeAttribute('disabled');
- getBoundingClientRect()
- 요소의 크기와 위치 정보를 담고 있는 객체를 반환한다. 이 정보는 요소의 크기와 위치를 조정할 때 유용하다.
- 예시:
const rect = elementRef.current.getBoundingClientRect();
- classList.add()/remove()/toggle()
- 요소의 클래스 목록을 조작한다. 클래스를 추가하거나 제거하거나 토글할 수 있다.
- 예시: elementRef.current.classList.add('active');
- value (속성)
<input>
,<textarea>
,<select>
와 같은 요소에서 값을 읽거나 쓸 수 있다.- 예시:
inputRef.current.value = 'new value'
;- innerHTML (속성)
- 요소의 HTML 콘텐츠를 읽거나 쓸 수 있다.
- 예시:
elementRef.current.innerHTML = '<p>Hello, World!</p>'
;- textContent (속성)
- 요소의 텍스트 콘텐츠를 읽거나 쓸 수 있다.
- 예시:
elementRef.current.textContent = 'Hello, World!'
;- style (속성)
- 요소의 인라인 스타일을 설정할 수 있다.
- 예시:
elementRef.current.style.color = 'red'
;
특정 상황에서 유용한 메서드들
- addEventListener(): 요소에 이벤트 리스너를 추가한다.
- removeEventListener(): 요소에서 이벤트 리스너를 제거한다.
- dispatchEvent(): 요소에서 특정 이벤트를 트리거한다.
이 외에도 요소의 타입에 따라 다양한 메서드와 속성에 접근할 수 있다. 예를 들어,
<video>
나<audio>
요소에는play()
,pause()
와 같은 미디어 제어 메서드가 있다. 따라서 useRef로 어떤 DOM 요소를 참조하느냐에 따라 사용할 수 있는 메서드와 속성이 달라진다.
고유한 ID를 생성하는 데 사용
이 ID는 클라이언트와 서버 사이에서 일관성을 유지하므로, 서버 사이드 렌더링(SSR)에서도 잘 동작한다.
기본 사용법
import React, { useId } from 'react';
function MyComponent() {
const id = useId();
return (
<div>
<label htmlFor={id}>이름</label>
<input id={id} type="text" />
</div>
);
}
이 예제에서 useId는 컴포넌트 내에서 고유한 id를 생성한다. 이 id는 <label>
과 <input>
요소를 연결하는 데 사용된다.
여러 개의 ID 생성
컴포넌트 내에서 여러 개의 고유한 ID를 생성해야 하는 경우, useId를 여러 번 호출하면 된다.
import React, { useId } from 'react';
function MyComponent() {
const id1 = useId();
const id2 = useId();
return (
<div>
<div>
<label htmlFor={id1}>이름</label>
<input id={id1} type="text" />
</div>
<div>
<label htmlFor={id2}>이메일</label>
<input id={id2} type="email" />
</div>
</div>
);
}
useId의 특징
요약
useId는 리액트 컴포넌트 내에서 고유한 ID를 생성하는 데 사용되는 훅이다. 고유성과 일관성을 보장하며, 서버 사이드 렌더링에서도 잘 동작한다. 여러 요소에 고유한 ID를 부여할 때 유용하게 사용할 수 있다.
버튼 비활성화 같은 UI 상태 관리
<form>
내부에서만 사용 가능<form>
자식 요소에서만 사용될 수 있다. <form>
이 선언된 컴포넌트에선 사용 불가const { pending, data, method, action } = useFormStatus();
<form>
이 아직 제출 중이라는 것을 의미<form>
이 제출한 데이터get
또는 post
, 어떤 HTTP 메서드
가 제출되는지 알려준다<form>
은 GET 메소드를 사용하고, method 속성을 통해 지정할 수 있다.<form>
의 action 속성에 전달된 함수에 대한 참조1.대기 상태 표시하기
주의점으로 인해 버튼을 컴포넌트로 만들고 form 태그에 넣을 것이다.
export default function FormBtn({ text }: FormBtnProps) {
const { pending } = useFormStatus();
return (
<Button disabled={pending} className="w-full">
{pending ? "로딩 중" : text}
</Button>
);
}
2. 제출된 form 데이터 읽기
App.tsx
import UsernameForm from './UsernameForm';
import { submitForm } from "./actions.js";
import {useRef} from 'react';
export default function App() {
const ref = useRef(null);
return (
<form ref={ref} action={async (formData) => {
await submitForm(formData);
ref.current.reset();
}}>
<UsernameForm />
</form>
);
}
UsernameForm.tsx
import {useState, useMemo, useRef} from 'react';
import {useFormStatus} from 'react-dom';
export default function UsernameForm() {
const {pending, data} = useFormStatus();
return (
<div>
<h3>Request a Username: </h3>
<input type="text" name="username" disabled={pending}/>
<button type="submit" disabled={pending}>
Submit
</button>
<br />
<p>{data ? `Requesting ${data?.get("username")}...`: ''}</p>
</div>
);
}
서버에서 반환한 데이터를 상태로 관리
폼 제출 시 비동기 작업을 수행하고, 상태를 관리하며, 로딩 상태를 체크할 수 있다.
useState
와 사용법이 비슷하다.useActionState(action, initialState, permalink?)
//action: 폼이 제출되거나 버튼을 눌렀을 때 호출될 함수
//initialState: 초기값, action에 들어갈 함수의 리턴값과 같아야 한다.
import { useActionState } from "react";
async function increment(previousState, formData) {
//previousState: 이전값이 없다면 null을 준다.
return previousState + 1;
}
function StatefulForm({}) {
const [state, formAction] = useActionState(increment, 0);
return (
<form>
{state}
<button formAction={formAction}>Increment</button>
</form>
)
}
넥스트js
Server Action과 함께 사용하는 경우, useActionState를 사용하여 hydration이 완료되기 전에도 폼 제출에 대한 서버의 응답을 보여줄 수 있다.
리액트에서 URL주소를 변경할 때 사용하는 Hook
url을 왔다갔다할 수 있다. 여러 route 사이를 움직일 수 있는 것
리액트 특성상, URL변경없이 내부 컴포넌트만 변경시켜 화면을 바꿔줄 수 있다.
하지만 URL을 바꿔주면 현재 어느 페이지에 있는지 대략적으로 알 수 있고,...
useHistory 사용법
비동기 통신 useSWR의 모든것
아래의 바운드와 언바운드의 각각의 장점
- 바운드 뮤테이트: 화면에서 얻은 데이터만 변경하기를 원할 때
- 언바운드 뮤테이트: 다른 화면의 데이트를 변경하기를 원할 때
만약 캐시 데이터를 변경하고 싶은게 아니라, 다시 한 번 요청해서 불러오고 싶다면 키값을 제외한 나머지 인자들을 전부 지우면 된다.
const { data, error, isLoading, isValidating, mutate } = useSWR('요청을 보낼 url', fetcher)
SWRConfig
ey값(URL)을 이용해 데이터를 가져오는 함수 fetcher는 어디서든 동일하게 사용될 것이다.
똑같은 기능의 fetcher 함수가 필요할때마다 선언하는건 비효율적일수도 있다.
이 경우 SWR Config를 이용해 전역적인 설정을 해 줄 수 있다.
해당 방법은 aixos를 적용한 것이므로 fetch를 사용한다면 여기를 참고하자.
여러 컴포넌트에서 같은 키 값을 사용해서 데이터를 불러온다고 했을 때 캐싱되는 시간(기본 5분) 내에선 api 호출을 두 번 하지 않는다.
state에 따라 api요청을 막고싶은 경우는?
// state에 따라 key 값을 null로 만들면 fetch가 실행되지 않습니다.
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
// key 함수가 falsy 값을 리턴하면 fetch가 실행되지 않습니다.
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
// user가 존재하지 않아서 user.id에 접근할 때 error가 발생해 fetch가 실행되지 않습니다.
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
서버상태 관리 라이브러리 선택: 리덕스, useSWR과 useQuery의 비교
기본 사용법
상태에 따라 API 요청 하지 않기
어떤 state에 따라 API 요청을 막고 싶은 경우
API 데이터 다시 가져오기
사용자가 ‘프로필 수정’ 기능을 통해 데이터를 수정해서, 프로필 정보를 refetch 하고 싶은 경우
API 요청 없이 데이터 업데이트 하기
refetch를 하지 않고 업데이트하고 싶은 경우
>__[useSWR이 리덕스를 대체할 수 있을까?](https://min9nim.vercel.app/2020-10-05-swr-intro2/)__
선요약:
모든 기능을 대체할 수 있지는 않겠지만 __Redux 를 사용하는 많은 경우에 있어서 번거로운 Redux 없이 SWR 만으로도 보다 쉽게 원하는 결과를 얻을 수 있을 것__
## SWRConfig
```javascript
const {mutate} = SWRConfig()
모든 SWR hook에 대한 전역 설정을 제공
mutate('/api/users/me', (prev:타입)=>({ok:prev.ok}), false) //mutate('키값', 변경할 부분, revalidation)
언바운드이기 때문에 어떤 데이터를 변경하는지 알아야 하므로 첫 번째 인자가 data가 아닌 key다.
변경할 부분에 prev=>를 설정한 이유는 바운드 함수와 다르게 변경해야할 이전 data가 존재하지 않기 때문이다.
이유는 첫 번째 인자로 data(이전 데이터)가 아닌 key값이 있어서 인데 prev=>를 하면 인자로 기존 데이터를 준다.
prev:타입=>({ok:prev.ok})
//바운드 함수도 가능
바운드처럼 {ok:false}로 로그아웃하는 것처럼 단지 바꿀 데이터 객체 하나만 보내는게 아니라, 이전 인자를 불러와서 변경했다.
데이터 패칭 라이브러리 - SWR (4. 중급 기능및 정리)
- Muatation
- 기본 사용법 (1) - useSWRConfig()
- 기본 사용법 (2) - useSWRMutation()
useSWRConfig()과 useSWRMutation()의 차이- 페이지 네이션(공식문서의 페이지네이션 탭 참고)
- 인피니트 로딩
- 정리
클라이언트 측의 캐시를 업데이트하는 데 사용한다. 이를 통해 서버에 실제로 요청을 보내지 않고도 UI를 실시간으로 업데이트할 수 있다.(좋아요 추가, 제거 등)
캐시 데이터 업데이트
mutate 함수는 특정 키에 대해 캐시된 데이터를 직접 업데이트할 수 있다. 이로 인해 서버에 요청을 보내지 않고도 UI를 즉시 업데이트할 수 있다.
함수 사용 형태 및 옵션 설명
mutate는 다음과 같은 형태로 사용한다:
mutate(key, data, shouldRevalidate);
각 매개변수의 역할:
실시간 데이터 반영
mutate를 사용하면 서버와의 통신이 완료되기 전에 UI를 즉시 업데이트할 수 있다.
예를 들어, 댓글 작성 후 새로운 댓글 데이터를 직접 캐시에 추가하여 UI가 즉시 업데이트되도록 할 수 있다.
이를 통해 사용자는 새로운 댓글이 실시간으로 추가된 것처럼 보게 된다.
mutate(
`/comments/${boardId}`,
(prev) => prev && ({
...prev,
data: [
...prev.data,
{
id: Date.now(),
parentCommentId: null,
childrenCommentsIds: [],
content,
},
],
}),
false
);
예제: 좋아요 기능
mutate 함수는 유저에게 바로 반응하는 최적화된 UI 제공에 유용하다. 예를 들어, 좋아요 기능을 구현할 때 사용된다.
boundMutate({ ...data, isLiked: !data.isLiked }, false);
또는
//prev &&는 타입스크립트
boundMutate((prev) => prev && ({ ...prev, isLiked: !prev.isLiked }), false);
캐시와 데이터 재검증
mutate를 호출하면, 기본적으로 캐시를 업데이트한 후 서버로부터 데이터를 다시 가져온다. 하지만 shouldRevalidate를 false로 설정하면 캐시만 업데이트하고, 서버로부터 데이터를 다시 가져오지 않는다.
이렇게 하면 사용자가 빠르게 변화를 볼 수 있지만, 데이터의 일관성을 유지하기 위해 적절한 시점에 서버와의 재동기화가 필요할 수 있다.
mutate(key);
이렇게 호출하면 단순히 데이터를 다시 가져와 캐시를 업데이트한다.
SWR의 캐시 관리
SWR은 super_cache라는 내부 캐시를 사용한다. 데이터는 요청한 API URL로 분류되어 저장된다. 이렇게 하면 다른 페이지에서 동일한 URL로 요청을 보낼 때, SWR이 캐시에서 데이터를 가져와 빠르게 응답할 수 있다.
super_cache = {
"/api/users/me": {
ok: true,
profile: {id: 9, phome: '12345', email: null,...}
}
}
위처럼 데이터는 요청한 api url로 분류되어 저장된다.(중요)
이로인해 다른 페이지에서 같은 url로 다시 요청을 보내면 SWR이 캐시에서 데이터를 가져온다.
공식문서 번역판
TanStack Query(React)에서 자주 사용하는 개념들을 정리한 저장소
설치는 #5.9참고
리액트 쿼리의 옵션과 결과값 정리
- 아주 많이 쉬워진 낙관적 업데이트
- 더이상 queryClient에서 suspense 모드를 설정해주지 않는다.
- on~ callback 시리즈 삭제(onSuccess 등)
- cacheTime은 gcTime로 바뀜
- useErrorBoundary는 throwOnError로 바뀜
{refetchInterval: 3000}
을 하면 3초마다 값을 갱신한다.예시:
const {isLoading, data} = useQuery(["allCoins"], fetchCoins)
isLoading:
ture 혹은 false인 불리언값, State로 설정했었던 로딩화면을 대체할 수 있다.
useQuery라는 훅이 fetcher함수를 불러오고, fetcher함수가 isLoading이라면, 그리고 fetcher함수가 끝난다면 리액트 쿼리가 알려준다.
그리고 fetchCoins가 끝나면 그 함수의 데이터를 위의 data에 넣어준다.
리액트 쿼리는 데이터를 캐시에 저장해두기에 화면을 이동해도 로딩이 보이지 않는다.
일반적으로 웹 애플리케이션에서 데이터를 불러오는 작업은 시간이 걸릴 수 있다.
예를 들어, 서버에서 데이터를 가져와야 하거나 API를 호출해야 할 때가 있다. 이런 경우 데이터를 불러오는 동안 화면에 로딩 표시를 보여주는 것이 일반적이다.
리액트 쿼리는 이러한 데이터 로딩을 효율적으로 처리하기 위해 데이터를 캐시에 저장한다. 즉, 한 번 불러온 데이터는 캐시에 저장되어 다음에 같은 데이터를 요청할 때 다시 서버에서 불러오지 않고 캐시에서 가져온다. 이로써 화면을 이동하거나 다른 페이지로 이동할 때도 로딩이 보이지 않게 된다.
react-query 캐싱 기능 제대로 이해하기const queryClient = new QueryClient({ defaultOptions: { queries: { // ✅ 글로벌 기본값은 20초입니다. staleTime: 1000 * 20, }, }, }) // 🚀 관련된 모든 작업은 1분의 오래된 시간을 갖게 됩니다. queryClient.setQueryDefaults(todoKeys.all, { staleTime: 1000 * 60 })
이를 통해 사용자는 빠른 응답 속도와 부드러운 화면 전환을 경험할 수 있다.
한 곳에 2개의 useQuery를 사용할 땐 아래와 같이 이름을 부여해준다.
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(
["info", coinId],
() => fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickersData } = useQuery<TickerData>(
["tickers", coinId],
() => fetchCoinTickers(coinId)
);
["tickers", coinId]
- 첫 번째 키가 카테고리의 역할
- 두 번째 키가 URL에 잇는 coinId가 되어서 고유한 부분이 되도록 했다.
["info", coinId]에서 coinId를 넣은 이유.
["info", coinId]
와["tickers", coinId]
는 쿼리의 키로 사용된다. 이 키는 쿼리 캐시(cache)를 식별하기 위해 사용된다. 만약에 두 쿼리가 동일한 키를 가지고 있다면, 쿼리 캐시에서 해당 데이터를 공유하게 된다.
따라서 coinId를 포함하여 키를 만드는 이유는, coinId가 변경될 때마다 해당 쿼리를 다시 실행하고 새로운 데이터를 가져오기 위함이다. 즉, 특정 코인의 정보를 가져오는 쿼리와 해당 코인의 티커 정보를 가져오는 쿼리가 각각 독립적으로 실행되지만, coinId가 변경되면 두 쿼리 모두 다시 실행되어 해당 코인에 대한 새로운 정보를 가져오게 된다.
세번째 인자:
refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)
숫자로 설정하면 밀리초 단위로 계속해서 refetch한다.
함수로 설정하면 최신 데이터로 함수가 실행되고 빈도를 계산하는 쿼리가 실행된다.
ex) useQuery(queryKey, queryFn ?, { refetchInterval })
https://react-query.tanstack.com/reference/useQuery#_top
리액트 쿼리의 캐싱기능: staleTime 속성과 cacheTime 속성
cacheTime:
캐시된 데이터가 메모리에 얼마나 오랫동안 유지될지를 제어
즉, 데이터가 캐시에 남아 있는 시간이다.
ㅤ
staleTime:
데이터가 "신선(fresh)"한 상태로 유지되는 기간을 설정
이 시간 동안은 React Query가 데이터를 신선한 상태로 간주하므로 자동으로 refetch하지 않고 캐시된 데이터를 그대로 사용한다.
리패치가 발생하는 상황을 일정 시간 동안 막는 역할이라 생각하면 쉽다.
ㅤReact Query에서 리패치(refetch)가 일어나는 상황
1. 데이터가 stale 상태일 때: staleTime이 지나 데이터가 stale 상태가 되면 리패치.
2. 컴포넌트가 다시 마운트될 때: 데이터가 stale 상태라면 리패치.
3. 윈도우 포커스가 돌아올 때: 포커스가 탭으로 돌아오고 데이터가 stale 상태라면 리패치.
4. 네트워크가 다시 연결될 때: 오프라인에서 온라인으로 전환되고 데이터가 stale 상태라면 리패치.
5. 주기적 리패치 설정 시: refetchInterval로 주기적으로 리패치.
6. invalidateQueries 호출 시: 쿼리가 무효화되면 리패치.
state에 따라 api요청을 막고싶은 경우 유용하다.
특정 데이터가 로드된 후에만 실행되도록 할 수 있다.
const { isIdle, data } = useQuery(["todos", userId], getTodosByUser, {
// 쿼리는 userId 값이 존재할 때까지 실행되지 않습니다
enabled: !!userId,
});
// isIdle 값은 enabled가 true가 될 때까지, fetch가 시작되기 전까지 true입니다.
React Query는 queryKey가 변경될 때마다 자동으로 쿼리를 다시 실행한다.
queryKey에 의존성을 포함시키면, 이 값이 변경될 때마다 쿼리가 다시 실행된다.
const { data, isLoading, error } = useQuery(['queryKey', dependency], fetchData);
주의점
['post']
쿼리키에서 데이터 변경이 일어나면['post', 1]
등은 안 되고 오로지['post']
만 된다.
post
를 사용하는 모든 키를 업데이트하고 싶다면 invalidateQueries 사용
eact Query의 반환값 중 하나인 refetch 함수를 사용하여 명시적으로 쿼리를 다시 실행할 수 있다.
예를 들어, 버튼 클릭과 같은 이벤트 핸들러 내에서 refetch를 호출할 수 있다.
const { data, isLoading, error, refetch } = useQuery('queryKey', fetchData);
// 필요한 상황에서 호출 (주로 post,patch 로직완료 후 )
refetch();
특정 쿼리 또는 모든 쿼리를 수동으로 무효화하여 다시 가져올 수 있다.
useQueryClient
훅을 사용하여 쿼리 클라이언트에 접근한 다음, invalidateQueries
메소드로 특정 쿼리를 무효화한다.
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery('queryKey', fetchData);
// 쿼리 무효화 및 다시 가져오기
queryClient.invalidateQueries('queryKey');
예를 들어, 유저가 post
, patch
를 발생시킴으로써 새로운 데이터가 생성되거나 기존 데이터가 수정된 후, 관련된 쿼리를 무효화하여 최신 상태의 데이터를 반영하고자 할 때 invalidateQueries
를 사용할 수 있다.
post, patch, delete
과 같이 데이터를 변경하는 로직 이후에 invalidateQueries
를 호출하면 변경된 데이터를 즉각적으로 바영하기 위해 해당 쿼리를 다시 실행시키고 캐시를 업데이트할 수 있다.
주의점
예를들어
['post']
쿼리키에서 데이터 변경이 일어나면['post', 1]
등post
를 사용하는 모든 부분에 대해 업데이트가 된다.
문제: 다음 페이지로 넘어갈 때 데이터를 불러오는 짦은 시간 동안 로딩상태가 되어 스켈레톤ui가 보임. 새로운 데이터가 패칭될 때까지 이전 캐시 데이터 유지가 필요
해결:
const { data: recordData, isLoading: isLoadingRecord } = useQuery({
queryFn: ({ headers }) =>
getDailyRecord(headers, {recordDate: date }),
queryKey: [API_GET_DAILY_RECORD_KEY, {recordDate: date }],
enabled: !!date,
//placeholderData: keepPreviousData, 잘못된 사용법, 2개 다 사용하고 싶으면 다음과 같이 각각 써야한다.
// 이전 데이터를 유지할 경우
keepPreviousData: true,
//더미 데이터를 사용할 경우
placeholderData: {
// 데이터가 처음 로드될 때 사용할 기본 데이터
data: {
records: [],
totalRecords: 0,
},
});
페이지로 돌아왔을 때 데이터를 자동으로 다시 패칭
사용자가 뒤로가기로 돌아오거나 다른 탭에서 돌아왔을 때 유용
useQuery('movieDetail', getMovieDetail, {
refetchOnWindowFocus: true, // 사용자가 페이지로 돌아올 때마다 데이터 재패칭
});
컴포넌트가 다시 마운트될 때 데이터를 패칭
사용자가 브라우저에서 뒤로가기로 돌아왔을 때 컴포넌트가 다시 마운트되면서 데이터를 재패칭하게 된다.
useQuery('movieDetail', getMovieDetail, {
refetchOnMount: true, // 기본값이 'always'로 설정되어 있어 마운트될 때마다 재패칭
});
네트워크 연결이 복구되었을 때 데이터를 다시 패칭하도록 설정
주로 네트워크 상태 변경 시 데이터를 다시 가져올 때 사용되지만, 뒤로가기 후 네트워크 상태가 변경될 경우를 대비해 설정할 수 있다.
useQuery('movieDetail', getMovieDetail, {
refetchOnReconnect: true,
});
react 버전이 18이면 타입스크립트에서 react query를 못 불러올 수도 있다.
npm i @tanstack/react-query
를 입력해서 모듈을 설치하면 react query불러오기가 가능해진다.
그리고@tanstack/react-query
에서 useQuery를 사용할때 query key의 값은 대괄호로 묶어줘야 한다.
react18과 react-query 4 버전 충돌에 따라
npm i @tanstack/react-query
을 참조하여, 수정한 코드다. 참고하자.
다양한 훅
Query 훅
- useQuery: 데이터 페칭을 위한 기본 훅
- useInfiniteQuery: 무한 스크롤 또는 페이지네이션 데이터를 페칭하는 데 사용
- useQueries: 여러 개의 쿼리를 동시에 실행할 때 사용
Mutation 훅- useMutation: 데이터 변형(예: POST, PUT, DELETE 요청)을 처리하는 데 사용
기타 훅
- useQueryClient: 쿼리 클라이언트를 사용하여 쿼리 캐시를 수동으로 조작할 때 사용
Geolocation API 사용하여 현재 내 위치 찾기 (with. TypeScript)
사용자의 위치 정보(위도, 경도)를 제공
import { useState, useEffect } from 'react';
interface locationType {
loaded: boolean;
coordinates?: { lat: number; lng: number };
error?: { code: number; message: string };
}
const useGeolocation = () => {
const [location, setLocation] = useState<locationType>({
loaded: false,
coordinates: { lat: 0, lng: 0, }
})
// 성공에 대한 로직
const onSuccess = (location: { coords: { latitude: number; longitude: number; }; }) => {
setLocation({
loaded: true,
coordinates: {
lat: location.coords.latitude,
lng: location.coords.longitude,
}
})
}
// 에러에 대한 로직
const onError = (error: { code: number; message: string; }) => {
setLocation({
loaded: true,
error,
})
}
useEffect(() => {
// navigator 객체 안에 geolocation이 없다면
// 위치 정보가 없는 것.
if (!("geolocation" in navigator)) {
onError({
code: 0,
message: "Geolocation not supported",
})
}
navigator.geolocation.getCurrentPosition(onSuccess, onError);
}, [])
return location;
}
export default useGeolocation
import useGeoLocation from "./hooks/useGeolocation";
function App() {
const location = useGeoLocation();
return (
<div className="App">
{location.loaded
? JSON.stringify(location)
: "Location data not available yet."}
</div>
);
}
export default App;
파라미터로 api 주소를 받아서 POST
import { useState } from "react";
interface UseMutationState<T> {
loading: boolean;
data?: T;
error?: object;
}
type UseMutationResult<T> = [(data: any) => void, UseMutationState<T>];
export default function useMutation<T = any>(
url: string
): UseMutationResult<T> {
const [state, setSate] = useState<UseMutationState<T>>({
loading: false,
data: undefined,
error: undefined,
});
function mutation(data: any) {
setSate((prev) => ({ ...prev, loading: true }));
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.json().catch(() => {}))
.then((data) => setSate((prev) => ({ ...prev, data })))
.catch((error) => setSate((prev) => ({ ...prev, error })))
.finally(() => setSate((prev) => ({ ...prev, loading: false })));
}
return [mutation, { ...state }];
}
interface MutationResult {
//응답 데이터의 타입
}
const [enter, { loading, data }] =
useMutation<MutationResult>("/api/users/enter");
.
.
.
const handleValue = (data: EnterForm) => {
if (loading) return;
enter(data);
};
재사용 가능한 Controller Input을 만들 수 있다.
ReactHookForm과 MUI 함께 사용할 때 유용
import { TextField } from "@mui/material";
import { Control, FieldValues, Path, useController } from "react-hook-form";
interface CustomInputProps<T extends FieldValues> {
control: Control<T>;
name: Path<T>;//import 위치 주의할 것
}
function CustomInput<T extends FieldValues>({
control,
name,
}: CustomInputProps<T>) {
const {
field,
fieldState: { invalid },
} = useController({
name,
control,
rules: { required: true },
});
return (
<TextField
onChange={field.onChange} // 폼에 값을 전달
onBlur={field.onBlur} // 입력이 터치/블러될 때 알림
value={field.value} // 입력 값
name={field.name} // 입력 필드의 이름 전달
inputRef={field.ref} // 입력 참조 전달, 에러 발생 시 입력에 포커스를 맞추기 위해 사용
error={invalid} // 필드가 유효하지 않을 경우 에러 표시
/>
);
}
export default CustomInput;
사용예시
interface IForm {
firstName: string;
}
function Home() {
const { handleSubmit, control } = useForm<IForm>();
const onSubmit = ({ firstName }: IForm) => {
console.log(firstName);
};
return (
<Layout>
<form onSubmit={handleSubmit(onSubmit)}>
<CustomInput control={control} name="firstName" />
</form>
</Layout>
);
이미지 파일을 업로드하고, 미리 보기를 간단히 구현
import { useState, useEffect } from "react";
function useImagePreview(initialImageUrl: string = "") {
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string>(initialImageUrl);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setImageFile(file);
} else {
//이미지가 이미 있을 때 이미지를 새로 선택 안 할 경우 취소
setImageFile(null); // 메모리 해제
setImagePreview(initialImageUrl);
}
};
//이미지 미리보기
useEffect(() => {
if (imageFile) {
const url = URL.createObjectURL(imageFile);
setImagePreview(url);
// 미리보기가 있을 때만 클린업 함수로 메모리 해제
return () => {
URL.revokeObjectURL(url);
};
}
}, [imageFile, initialImageUrl]);
return { imageFile, imagePreview, handleFileChange };
}
export default useImagePreview;
{ name: "profile.png", size: 34567, type: "image/png", ... }
"blob:http://localhost:3000/..."
initialImageUrl
을 반환<input type="file" />
요소의 onChange 이벤트
에 사용사용예시
회원정보 수정 페이지에서 프로필 사진을 수정하는 일부 예시
const { imageFile, imagePreview, handleFileChange } = useImagePreview(
"/default-image.png" // 초기 이미지 URL
);
const onEditSubmit = (data: IForm) => {
const formData = new FormData();
if (imageFile) {
formData.append("avatar", imageFile); // 파일이 있을 경우 avatar로 추가
}
mutate(formData); // FormData 객체를 서버로 전송
};
return (
<form onSubmit={handleSubmit(onEditSubmit)}>
<div className="flex flex-col items-center">
<label
htmlFor="avatar"
className="flex items-center justify-center w-16 rounded-full aspect-square"
>
<img
className="object-cover w-16 rounded-full aspect-square"
src={imagePreview || defaultAvatarImgUrl} //화면에 미리보기 이미지 보여주기
/>
<input
type="file"
accept="image/*"
id="avatar"
onChange={handleFileChange} //파일 선택 이벤트 핸들러
ref={hiddenInputRef}
className="hidden"
/>
</label>
)
특정 URL로 사용자를 리다이렉트(네비게이트)
주로 외부 페이지로 이동하는 상황에서 사용
import { useState, useEffect } from "react";
const useNavigateToExternal = (url: string) => {
const [navigate, setNavigate] = useState(false);
useEffect(() => {
if (navigate) {
window.location.href = url;
}
}, [navigate, url]);
const triggerNavigation = () => {
setNavigate(true);
};
return triggerNavigation;
};
export default useNavigateToExternal;
navigate
상태를 true
로 변경하여 리다이렉트를 트리거사용 예시
버튼 클릭 시 외부 사이트로 이동
const redirectToGoogle = useNavigateToExternal("https://google.com");
return (
<button onClick={redirectToGoogle}>
Google로 이동
</button>
);
동적 컴포넌트를 로드(즉, 필요할 때만 가져옴)하는 데 사용
React.lazy를 기반으로 하며, 컴포넌트를 지연 로드할 뿐만 아니라 미리 로드(preload) 기능을 제공
예시: 마우스를 hover할 때 미리 로드
Lazy-loading Component가 많은 경우에 getLazyComponentWithPreload 함수를 재사용해 코드 효율성을 높일 수 있다
// useLazyComponent.ts
import { lazy, ComponentType, LazyExoticComponent } from 'react';
export type ReactLazyFactory<T = any> = () => Promise<{ default: ComponentType<T> }>;
export type ComponentPreloadTuple<T = any> = [component: LazyExoticComponent<ComponentType<T>>, preloadFn: () => void];
export function getLazyComponentWithPreload<T = any>(componentPath: string): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload<T = any>(factory: ReactLazyFactory<T>): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload<T = any>(input: string | ReactLazyFactory<T>): ComponentPreloadTuple<T> {
const factory = () => (typeof input === 'string' ? import(input) : input());
return [lazy(factory), factory];
}
1. 동적 import: 컴포넌트를 동적으로 로드하여 초기 번들 크기를 줄이고 필요한 시점에 로드.
2. 미리 로드: 특정 조건에서 컴포넌트를 미리 로드하여 사용자 경험 향상.
컴포넌트를 동적으로 로드할 때 문자열 경로나 팩토리 함수(컴포넌트를 반환하는 함수)를 전달할 수 있다.
1-1. 문자열 경로를 사용하는 경우
import { getLazyComponentWithPreload } from "./getLazyComponentWithPreload";
const [LazyComponent, preloadLazyComponent] = getLazyComponentWithPreload(
"./components/MyComponent"
);
// LazyComponent는 React.lazy로 생성된 컴포넌트
// preloadLazyComponent는 해당 컴포넌트를 미리 로드하는 함수
1-2. 팩토리 함수를 사용하는 경우
const factory = () =>
import("./components/MyComponent").then((module) => ({
default: module.MyComponent,
}));
const [LazyComponent, preloadLazyComponent] = getLazyComponentWithPreload(factory);
LazyComponent는 React.lazy를 사용하여 만든 컴포넌트다.
이를 리액트의 Suspense와 함께 사용해야 한다.
import React, { Suspense } from "react";
function App() {
const [LazyComponent] = getLazyComponentWithPreload("./components/MyComponent");
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
3. Preload 함수 사용
preloadFn
을 호출하여 컴포넌트를 미리 로드할 수 있다.
사용자 경험을 향상시키기 위해 특정 이벤트(예: 버튼 클릭, 페이지 전환 전)에 호출할 수 있다.
const [LazyComponent, preloadLazyComponent] = getLazyComponentWithPreload(
"./components/MyComponent"
);
function handleMouseEnter() {
// 사용자가 마우스를 올렸을 때 컴포넌트를 미리 로드
preloadLazyComponent();
}
function App() {
return (
<div onMouseEnter={handleMouseEnter}>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
<T>
: 동적 import를 생성하는 팩토리 함수.[LazyComponent, preloadFn]
:조건부 미리 로드
페이지 전환 전, 특정 컴포넌트를 미리 로드하고 싶을 때 사용
import { getLazyComponentWithPreload } from "./getLazyComponentWithPreload";
import { useEffect } from "react";
const [LazyPage, preloadLazyPage] = getLazyComponentWithPreload(
"./pages/NextPage"
);
function App() {
useEffect(() => {
// 컴포넌트를 미리 로드 (예: 페이지 전환 전에 호출)
preloadLazyPage();
}, []);
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyPage />
</Suspense>
);
}