useEffect의 실제 동작 방식을 React 소스 코드를 통해 살펴보겠습니다. 이를 통해 useEffect가 어떻게 작동하는지, 그리고 React의 내부 메커니즘과 어떻게 연결되는지 이해할 수 있습니다.
먼저 packages/react/src/ReactHooks.js
파일에서 useEffect를 볼 수 있습니다:
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
이 코드는 useEffect를 정의합니다. 여기서 주목할 점은:
create
함수: 이펙트를 실행할 함수입니다. 이 함수는 선택적으로 클린업 함수를 반환할 수 있습니다.deps
배열: 의존성 배열로, 이 값들이 변경될 때만 이펙트가 재실행됩니다.resolveDispatcher()
: 현재 React의 렌더링 단계에 따라 적절한 디스패처를 반환합니다.dispatcher.useEffect
는 실제 구현을 가리키는데, 이는 렌더링 단계(마운트 또는 업데이트)에 따라 다른 함수를 호출합니다.
packages/react-reconciler/src/ReactFiberHooks.js
파일에서 useEffect의 실제 구현을 볼 수 있습니다:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps
);
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps
);
}
여기서 주목할 점:
mountEffect
: 컴포넌트가 처음 마운트될 때 호출됩니다.updateEffect
: 컴포넌트가 업데이트될 때 호출됩니다.UpdateEffect | PassiveEffect
: 이펙트의 타입을 지정합니다. PassiveEffect는 useEffect가 비동기적으로 실행됨을 나타냅니다.HookPassive
: 훅의 타입을 나타냅니다.이 함수들은 mountEffectImpl
과 updateEffectImpl
을 호출하여 실제 이펙트 로직을 처리합니다.
의존성 배열의 변화를 감지하는 areHookInputsEqual
함수를 살펴보겠습니다:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
이 함수는:
Object.is
알고리즘으로 비교합니다.이 과정을 통해 불필요한 이펙트 실행을 방지합니다.
이펙트의 실행과 클린업은 commitHookEffectListMount
와 commitHookEffectListUnmount
함수에서 처리됩니다:
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
이 함수들은:
commitHookEffectListMount
는 이펙트 함수를 실행하고, 반환된 클린업 함수를 저장합니다.commitHookEffectListUnmount
는 저장된 클린업 함수를 실행합니다.이러한 메커니즘을 통해 React는 useEffect의 생명주기를 관리하고, 컴포넌트의 마운트, 업데이트, 언마운트 시점에 적절한 동작을 수행합니다.
useEffect
를 효과적으로 사용하기 위해서는 다음과 같은 고급 최적화 기법을 고려해야 합니다. 각 기법에 대해 상세히 살펴보겠습니다.
객체나 배열을 의존성 배열에 포함시킬 때는 불변성을 유지하여 불필요한 재렌더링을 방지해야 합니다.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(userData => {
setUser(userData);
});
}, [userId]);
useEffect(() => {
// 이 효과는 user 객체가 변경될 때마다 실행됩니다
console.log('User updated:', user);
}, [user]); // user는 객체이므로 매번 새로운 참조가 생성됩니다
// ...
}
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(userData => {
setUser(userData);
});
}, [userId]);
useEffect(() => {
// 이 효과는 user의 특정 속성이 변경될 때만 실행됩니다
console.log('User name updated:', user?.name);
}, [user?.name]); // user.name이 변경될 때만 효과가 실행됩니다
// ...
}
이 방식은 user
객체 전체가 아닌 필요한 속성만 의존성 배열에 포함시켜 불필요한 효과 실행을 방지합니다.
useCallback
을 사용하여 함수를 메모이제이션하면 불필요한 효과 재실행을 줄일 수 있습니다.
function SearchComponent({ onSearch }) {
const [query, setQuery] = useState('');
// onSearch가 변경될 때마다 이 효과가 재실행됩니다
useEffect(() => {
const delayedSearch = setTimeout(() => {
onSearch(query);
}, 500);
return () => clearTimeout(delayedSearch);
}, [query, onSearch]); // onSearch가 부모 컴포넌트에서 매번 새로 생성된다면 문제가 됩니다
// ...
}
// 부모 컴포넌트
function ParentComponent() {
// 이 함수는 매 렌더링마다 새로 생성됩니다
const handleSearch = (query) => {
// 검색 로직
};
return <SearchComponent onSearch={handleSearch} />;
}
개선된 버전:
function SearchComponent({ onSearch }) {
const [query, setQuery] = useState('');
const debouncedSearch = useCallback(
(q) => {
const delayedSearch = setTimeout(() => {
onSearch(q);
}, 500);
return () => clearTimeout(delayedSearch);
},
[onSearch]
);
useEffect(() => {
return debouncedSearch(query);
}, [query, debouncedSearch]);
// ...
}
// 부모 컴포넌트
function ParentComponent() {
const handleSearch = useCallback((query) => {
// 검색 로직
}, []); // 의존성이 없으므로 한 번만 생성됩니다
return <SearchComponent onSearch={handleSearch} />;
}
이 방식은 handleSearch
함수를 메모이제이션하여 SearchComponent
의 불필요한 리렌더링과 효과 재실행을 방지합니다.
비동기 작업을 다룰 때는 레이스 컨디션을 방지하고, 필요하다면 작업을 취소할 수 있어야 합니다.
function UserData({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
let isCancelled = false;
async function fetchData() {
try {
const response = await fetch(`https://api.example.com/user/${userId}`);
const result = await response.json();
if (!isCancelled) {
setData(result);
}
} catch (error) {
if (!isCancelled) {
console.error('Failed to fetch user data:', error);
}
}
}
fetchData();
return () => {
isCancelled = true;
};
}, [userId]);
// ...
}
이 패턴은 컴포넌트가 언마운트되거나 userId
가 변경되어 새로운 요청이 시작될 때 이전 요청의 결과를 무시합니다. 이를 통해 레이스 컨디션을 방지하고 메모리 누수를 막을 수 있습니다.
빈번한 상태 업데이트나 API 호출을 최적화하기 위해 디바운싱이나 스로틀링 기법을 활용할 수 있습니다.
import { debounce } from 'lodash';
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const debouncedSearch = useCallback(
debounce(async (term) => {
const response = await fetch(`https://api.example.com/search?q=${term}`);
const data = await response.json();
setResults(data);
}, 300),
[]
);
useEffect(() => {
debouncedSearch(searchTerm);
return () => debouncedSearch.cancel();
}, [searchTerm, debouncedSearch]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{/* 결과 표시 */}
</div>
);
}
이 예시에서는 lodash
의 debounce
함수를 사용하여 검색 요청을 최적화합니다. 사용자가 입력을 멈춘 후 300ms가 지나면 검색이 실행됩니다.
import { throttle } from 'lodash';
function ScrollTracker() {
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = useCallback(
throttle(() => {
const position = window.pageYOffset;
setScrollPosition(position);
}, 100),
[]
);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
handleScroll.cancel();
};
}, [handleScroll]);
return <div>Scroll position: {scrollPosition}</div>;
}
이 예시에서는 스크롤 이벤트 처리를 100ms마다 한 번씩만 실행하도록 스로틀링합니다. 이를 통해 스크롤 위치 업데이트의 빈도를 제한하여 성능을 개선할 수 있습니다.
이러한 최적화 기법들을 적절히 활용하면 useEffect
의 성능을 크게 향상시킬 수 있습니다. 하지만 각 기법의 장단점을 이해하고, 실제 애플리케이션의 요구사항에 맞게 적용하는 것이 중요합니다. 또한, 과도한 최적화는 코드의 복잡성을 증가시킬 수 있으므로, 실제 성능 문제가 있는 경우에만 이러한 기법을 적용하는 것이 좋습니다.
useEffect는 다양한 복잡한 시나리오에서 활용될 수 있습니다. 여기서는 세 가지 주요 시나리오를 자세히 살펴보겠습니다.
여러 상태를 동기화해야 하는 경우, useEffect를 활용하여 상태 간의 일관성을 유지할 수 있습니다. 이는 특히 하나의 상태 변경이 다른 상태에 영향을 미치는 경우에 유용합니다.
장바구니의 아이템 목록과 총 가격을 동기화하는 시나리오를 생각해봅시다.
function ShoppingCart() {
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
const [discount, setDiscount] = useState(0);
// 아이템 목록이 변경될 때마다 총 가격 업데이트
useEffect(() => {
const newTotalPrice = items.reduce((sum, item) => sum + item.price, 0);
setTotalPrice(newTotalPrice);
}, [items]);
// 총 가격이 변경될 때마다 할인 적용
useEffect(() => {
if (totalPrice > 100) {
setDiscount(totalPrice * 0.1); // 10% 할인
} else {
setDiscount(0);
}
}, [totalPrice]);
const addItem = (newItem) => {
setItems(prevItems => [...prevItems, newItem]);
};
return (
<div>
{/* 장바구니 UI 렌더링 */}
<p>Total Price: ${totalPrice}</p>
<p>Discount: ${discount}</p>
</div>
);
}
이 예시에서는 두 개의 useEffect를 사용하여 상태 간의 복잡한 관계를 관리합니다:
1. 첫 번째 useEffect는 아이템 목록이 변경될 때마다 총 가격을 재계산합니다.
2. 두 번째 useEffect는 총 가격이 변경될 때마다 할인을 적용합니다.
이러한 접근 방식의 장점은 각 상태 변경의 로직을 분리하여 관리할 수 있다는 것입니다. 하지만 주의할 점은 순환 종속성을 만들지 않도록 해야 한다는 것입니다.
React 18에서는 새로운 동시성 기능과 함께 네트워크 요청을 다루는 새로운 패턴이 도입되었습니다. 이 패턴은 레이스 컨디션을 방지하고 불필요한 상태 업데이트를 막아줍니다.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchUserData() {
setLoading(true);
setError(null);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
if (!ignore) {
setUser(userData);
}
} catch (err) {
if (!ignore) {
setError(err);
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
fetchUserData();
return () => {
ignore = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
이 패턴의 주요 특징:
1. ignore
플래그를 사용하여 컴포넌트가 언마운트되거나 userId가 변경된 후의 응답을 무시합니다.
2. 클린업 함수에서 ignore
를 true로 설정하여 불필요한 상태 업데이트를 방지합니다.
3. 로딩 상태와 에러 처리를 포함하여 사용자 경험을 향상시킵니다.
이 방식은 특히 빠르게 변경되는 데이터나 사용자 입력에 따라 데이터를 가져오는 경우에 유용합니다.
전역 이벤트 리스너를 다룰 때, 성능 최적화를 위해 디바운싱이나 스로틀링을 사용할 수 있습니다. 이는 이벤트 핸들러가 너무 자주 호출되는 것을 방지합니다.
import { throttle } from 'lodash';
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = throttle(() => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 200); // 200ms마다 최대 한 번씩 실행
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
handleResize.cancel(); // throttle 함수의 취소
};
}, []); // 빈 의존성 배열
return (
<div>
<p>Window width: {windowSize.width}px</p>
<p>Window height: {windowSize.height}px</p>
</div>
);
}
이 예시의 주요 특징:
1. lodash
의 throttle
함수를 사용하여 리사이즈 이벤트 처리의 빈도를 제한합니다.
2. 클린업 함수에서 이벤트 리스너를 제거하고 throttle 함수를 취소합니다.
3. 빈 의존성 배열을 사용하여 이펙트가 한 번만 설정되도록 합니다.
이 방식은 빈번한 이벤트(스크롤, 마우스 이동 등)를 처리할 때 특히 유용하며, 애플리케이션의 전반적인 성능을 향상시킬 수 있습니다.
각 시나리오에서 useEffect를 사용할 때는 항상 부작용을 최소화하고, 필요한 경우에만 상태를 업데이트하며, 적절한 클린업을 수행하는 것이 중요합니다. 이러한 패턴들을 잘 활용하면 복잡한 상황에서도 React 애플리케이션의 동작을 효과적으로 제어할 수 있습니다.
React 18은 몇 가지 중요한 변화를 도입했으며, 이는 useEffect
의 동작에 상당한 영향을 미칩니다. 주요 변경사항으로는 자동 배칭(Automatic Batching)과 Strict Mode에서의 이중 호출이 있습니다.
React 18의 자동 배칭은 여러 상태 업데이트를 하나의 리렌더링으로 묶어줍니다. 이는 성능을 향상시키지만, useEffect
의 실행 타이밍에 영향을 줄 수 있습니다.
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
useEffect(() => {
console.log(`Effect ran. Count: ${count}, Flag: ${flag}`);
}, [count, flag]);
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 18 이전: 이 함수가 호출될 때 useEffect가 두 번 실행됨
// React 18: 이 함수가 호출될 때 useEffect는 한 번만 실행됨
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {String(flag)}</p>
<button onClick={handleClick}>Update</button>
</div>
);
}
이 예시에서:
handleClick
함수가 호출될 때 setCount
와 setFlag
가 각각 리렌더링을 트리거하여 useEffect
가 두 번 실행되었습니다.useEffect
는 한 번만 실행됩니다.flushSync
를 사용하여 배칭을 강제로 해제할 수 있습니다.import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// 여기서 DOM이 업데이트됨
flushSync(() => {
setFlag(f => !f);
});
// 여기서 다시 DOM이 업데이트됨
}
하지만 flushSync
의 사용은 성능에 부정적인 영향을 줄 수 있으므로 꼭 필요한 경우에만 사용해야 합니다.
React 18의 Strict Mode에서는 개발 중 useEffect
가 두 번 호출됩니다. 이는 부작용을 찾고 컴포넌트의 회복력을 테스트하기 위한 것으로, 프로덕션 빌드에서는 발생하지 않습니다.
function DataFetcher({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
console.log('Effect ran');
let ignore = false;
async function fetchData() {
const response = await fetch(url);
const result = await response.json();
if (!ignore) {
setData(result);
}
}
fetchData();
return () => {
console.log('Cleanup ran');
ignore = true;
};
}, [url]);
return data ? <div>{JSON.stringify(data)}</div> : <div>Loading...</div>;
}
Strict Mode에서 이 컴포넌트를 마운트하면 콘솔에 다음과 같이 출력됩니다:
1. 'Effect ran'
2. 'Cleanup ran'
3. 'Effect ran'
목적:
대응 방법:
ignore
플래그 사용)주의사항:
function LocalStorageManager({ key, initialValue }) {
const [value, setValue] = useState(() => {
return JSON.parse(localStorage.getItem(key)) || initialValue;
});
useEffect(() => {
console.log(`Saving ${key} to localStorage`);
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
이 예시에서 Strict Mode는 로컬 스토리지에 두 번 저장하는 동작을 유발할 수 있습니다. 이는 실제 문제를 일으키지는 않지만, 개발자가 부수 효과의 중복 실행에 대비해야 함을 보여줍니다.
React 18의 이러한 변화들은 애플리케이션의 안정성과 성능을 향상시키는 데 도움을 주지만, 개발자들이 useEffect
를 더욱 신중하게 사용해야 함을 의미합니다. 부수 효과를 최소화하고, 필요한 경우에만 사용하며, 항상 적절한 정리(cleanup) 로직을 구현하는 것이 중요합니다.
useEffect를 효과적으로 사용하기 위한 최신 트렜드와 best practices를 살펴보겠습니다. 이러한 접근 방식들은 코드의 재사용성, 유지보수성, 그리고 테스트 용이성을 크게 향상시킬 수 있습니다.
복잡한 useEffect 로직은 커스텀 훅으로 추상화하는 것이 좋습니다. 이는 로직의 재사용성을 높이고, 컴포넌트의 가독성을 개선합니다.
function usePaginatedData(baseUrl, pageSize = 10) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
let ignore = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(`${baseUrl}?page=${page}&limit=${pageSize}`);
const newData = await response.json();
if (!ignore) {
setData(prevData => [...prevData, ...newData]);
setHasMore(newData.length === pageSize);
setLoading(false);
}
} catch (error) {
if (!ignore) {
setError(error);
setLoading(false);
}
}
}
fetchData();
return () => {
ignore = true;
};
}, [baseUrl, page, pageSize]);
const loadMore = useCallback(() => {
if (!loading && hasMore) {
setPage(prevPage => prevPage + 1);
}
}, [loading, hasMore]);
return { data, loading, error, hasMore, loadMore };
}
// 사용 예시
function PostList() {
const { data: posts, loading, error, hasMore, loadMore } = usePaginatedData('https://api.example.com/posts', 20);
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts.map(post => <PostItem key={post.id} post={post} />)}
{loading && <div>Loading...</div>}
{hasMore && <button onClick={loadMore} disabled={loading}>Load More</button>}
</div>
);
}
useCallback
과 useMemo
를 적절히 사용하세요.useEffect 내에서 복잡한 상태 업데이트 로직은 useReducer로 분리하는 것이 좋습니다. 이 접근 방식은 상태 업데이트 로직을 중앙화하고 테스트하기 쉽게 만듭니다.
// 리듀서 정의
function formReducer(state, action) {
switch (action.type) {
case 'FIELD_CHANGE':
return { ...state, [action.field]: action.value, touched: { ...state.touched, [action.field]: true } };
case 'SET_ERRORS':
return { ...state, errors: action.errors };
case 'SUBMIT_START':
return { ...state, isSubmitting: true };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false, submitSuccess: true };
case 'SUBMIT_FAILURE':
return { ...state, isSubmitting: false, submitError: action.error };
default:
return state;
}
}
// 커스텀 훅
function useComplexForm(initialState, validateForm, submitForm) {
const [state, dispatch] = useReducer(formReducer, {
...initialState,
touched: {},
errors: {},
isSubmitting: false,
submitSuccess: false,
submitError: null
});
useEffect(() => {
const errors = validateForm(state);
dispatch({ type: 'SET_ERRORS', errors });
}, [state.name, state.email, state.message]);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({ type: 'SUBMIT_FAILURE', error });
}
};
const handleFieldChange = (field, value) => {
dispatch({ type: 'FIELD_CHANGE', field, value });
};
return { state, handleFieldChange, handleSubmit };
}
// 사용 예시
function ContactForm() {
const { state, handleFieldChange, handleSubmit } = useComplexForm(
{ name: '', email: '', message: '' },
validateForm,
submitForm
);
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={state.name}
onChange={(e) => handleFieldChange('name', e.target.value)}
/>
{state.touched.name && state.errors.name && <span>{state.errors.name}</span>}
{/* 다른 필드들... */}
<button type="submit" disabled={state.isSubmitting}>Submit</button>
{state.submitSuccess && <p>Form submitted successfully!</p>}
{state.submitError && <p>Error: {state.submitError.message}</p>}
</form>
);
}
비동기 작업을 다룰 때 레이스 컨디션을 방지하는 것은 매우 중요합니다. 특히 데이터 fetching 시나리오에서 이 문제가 자주 발생합니다.
function useUserSearch() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let isCurrent = true;
const controller = new AbortController();
async function fetchUsers() {
if (searchTerm.length < 3) {
setResults([]);
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(`https://api.example.com/users?search=${searchTerm}`, {
signal: controller.signal
});
const data = await response.json();
if (isCurrent) {
setResults(data);
setLoading(false);
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else if (isCurrent) {
setError(error);
setLoading(false);
}
}
}
fetchUsers();
return () => {
isCurrent = false;
controller.abort();
};
}, [searchTerm]);
return { searchTerm, setSearchTerm, results, loading, error };
}
// 사용 예시
function UserSearch() {
const { searchTerm, setSearchTerm, results, loading, error } = useUserSearch();
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search users..."
/>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
이러한 best practices를 적용하면 useEffect를 사용하는 코드의 품질과 유지보수성을 크게 향상시킬 수 있습니다. 하지만 각 접근 방식의 장단점을 이해하고, 프로젝트의 요구사항에 맞게 적절히 사용하는 것이 중요합니다.
useEffect는 React 애플리케이션에서 부수 효과를 관리하는 강력한 도구입니다. 이렇게 React의 내부 구현을 살펴봄으로써, useEffect가 어떻게 작동하는지 더 깊이 이해할 수 있습니다. React는 복잡한 내부 로직을 통해 개발자에게 간단한 API를 제공하면서도, 효율적인 상태 관리와 부수 효과 처리를 가능하게 합니다. 이러한 이해를 바탕으로 우리는 useEffect를 더 효과적으로 사용하고 최적화할 수 있습니다.
또한, React의 생태계는 계속해서 진화하고 있으며, 이에 따라 useEffect의 사용 방식도 변화할 것입니다. 따라서 최신 트렌드와 패턴을 지속적으로 학습하고, 실제 프로젝트에 적용해보는 것이 중요합니다.