❓ _
의 역할 – map에서 _
는 뭘까?
1
2
3
4
5
| skeletonArr.map((_, i) => (
<li key={i}>
<ProductCardSkeleton />
</li>
))
|
map((_, i) => ...)
에서 _
는 사용하지 않을 첫 번째 인자(요소)를 의미- 보통
.map((item, index) => ...)
처럼 쓰지만, item
을 사용하지 않으면 _
로 대체해서 의도를 명확히 표현할 수 있음
→ 즉, “나는 이 인자는 쓰지 않을 거야!” 라는 의미 !
❓ debounce vs throttle
- 스크롤/입력/리사이즈 등 연속적으로 실행되는 이벤트를 조절해 성능 최적화
- 💡 이벤트 발생을 제한해서 성능을 최적화하는 개념
✅ 디바운싱(Debouncing)
- 연속적으로 발생하는 이벤트들 중 마지막 이벤트만 처리하는 기법
- 일정 시간 내에 추가 이벤트가 발생하면 타이머를 재설정하고, 더 이상 이벤트가 발생하지 않을 때 최종적으로 함수를 실행
예시 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| function debounce(func, delay) {
let timeoutId;
return function(...args) {
// 이전 타이머가 있으면 취소
if (timeoutId) clearTimeout(timeoutId);
// 새로운 타이머 설정
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 사용 예시: 검색창 입력 디바운싱
const searchInput = document.getElementById('search');
const handleSearch = (e) => {
console.log('Searching for:', e.target.value);
// API 호출 등의 작업
};
// 300ms 동안 추가 입력이 없을 때만 검색 수행
searchInput.addEventListener('input', debounce(handleSearch, 300));
|
✅ 스로틀링(Throttling)
- 일정 시간 간격으로 함수를 최대 한 번만 실행하도록 제한하는 기법
- 디바운싱과 달리 마지막 이벤트를 기다리지 않고, 정해진 주기마다 한 번씩 함수를 실행
예시 코드
1
2
3
4
5
6
7
8
9
10
| export const throttle = (func, limit = 300) => {
let inThrottle
return function (...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
|
resize 이벤트가 연속적으로 발생해도 1초에 한 번만 실행
1
2
3
4
5
| const handleResize = throttle(() => {
if (window.innerWidth > 1100) {
setIsOn(false)
}
}, 1000)
|
→ 리사이즈 시 화면 너비가 1100보다 크면 isOn
을 꺼주는데, 이걸 1초에 한 번만 실행하도록 조절
debounce와 throttle의 차이
개념 | 디바운스 | 쓰로틀 |
---|
언제 실행? | 마지막 이벤트 후 한 번만 실행 | 일정 시간마다 실행 |
예시 상황 | 검색창 자동완성 (타이핑이 끝난 후 실행) | 스크롤 위치에 따라 UI 변경 (지속적으로 실행) |
실제 프로젝트에서는 Lodash나 Underscore 같은 라이브러리의 구현체를 많이 사용
❓ React Router- loader
의 역할
- 페이지 렌더링 전에 데이터를 미리 받아오는 함수
useLoaderData()
로 페이지에서 받아올 수 있음useEffect
보다 선제적으로 동작함 → 초기 로딩 시점 최적화
1
2
3
4
5
6
7
8
9
10
11
12
| {
path: '/detail/:productId',
element: <DetailPage />,
loader: async ({ params }) => {
try {
const product = await getProductById(params.productId)
return product
} catch (err) {
console.log('err----', err)
}
},
}
|
- DetailPage가 열리기 전에 productId에 해당하는 상품 데이터를 먼저 받아와서 넘겨줌.
- 이렇게 하면 컴포넌트 안에서
useEffect
로 따로 불러오는 게 아니라, 라우터에서 데이터를 미리 처리
❓ Vite 설정 – alias & proxy
🔹 alias
1
2
3
| alias: {
'@': path.resolve('./src'),
}
|
@
를 src
로 매핑해서 import
경로를 더 간결하게 쓸 수 있음@/components/ComponentName
처럼 사용 가능
✅ proxy
개발 환경에서는 CORS(Cross-Origin Resource Sharing) 문제가 생길 수 있다.
브라우저 보안 정책에 따라,
예를 들어 프론트엔드가 localhost:5173
에서 실행 중이고
API 서버가 localhost:3000
일 경우,
직접 요청(5173 → 3000)은 보안상 차단된다.
이럴 때 Vite
에서 proxy
를 설정하면,
프론트엔드 서버를 통해 API 요청을 우회시킬 수 있다 !
브라우저는 이를 같은 출처(origin)에서의 요청으로 인식하게 되어,
CORS 오류 없이 API 요청이 정상적으로 작동한다.
1
2
3
4
5
6
7
8
9
| server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
|
/api
로 시작하는 모든 요청은 http://localhost:3000
로 리다이렉트됨changeOrigin: true
는 호스트 헤더를 대상 URL의 호스트 이름으로 변경하여 CORS 문제를 방지rewrite
함수는 URL에서 /api
접두어를 제거/api/products
를 요청하면 서버에는 http://localhost:3000/products
로 전달
전체 Vite 설정 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve('./src'),
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})
|
❓ TypeScript – 경로 별칭 설정 (tsconfig.json)
1
2
3
4
5
6
7
8
| {
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
}
}
}
|
- TypeScript에서도 @ 경로로 import 가능!
import { Button } from '@/components/Button'
처럼 가독성도 좋아지고 유지보수도 쉬워짐.
💡 왜 tsconfig에도 alias 설정이 필요한가?
설정 대상 | 역할 |
---|
vite.config.js | 실행 중인 Vite 개발 서버용 |
tsconfig.json | TypeScript 컴파일러와 VS Code가 경로 인식 |
둘 다 설정해야 import
, 자동완성, 타입 검사 모두 완벽하게 작동!
상품 상세 페이지 (실습 + 과제)
END