importReact,{useState,useCallback}from'react';function텍스트에디터(){const[텍스트크기,set텍스트크기]=useState(16);const[텍스트색상,set텍스트색상]=useState('black');// 1. 빈 의존성 배열 - 컴포넌트 마운트 시 한 번만 생성됨
const기본설정적용=useCallback(()=>{set텍스트크기(16);set텍스트색상('black');},[]);// 빈 배열: 함수가 컴포넌트 생명주기 동안 유지됨
// 2. 의존성이 있는 경우: `색상변경` 함수는 `텍스트크기`에 의존합니다.
const색상변경=useCallback((새색상)=>{console.log(`${텍스트크기}px 크기의 텍스트 색상을 ${새색상}으로 변경합니다`);set텍스트색상(새색상);},[텍스트크기]);// 텍스트크기가 변경될 때만 함수가 재생성됨
return(<divstyle={{padding:'20px',border:'1px solid gray'}}><pstyle={{fontSize:`${텍스트크기}px`,color:텍스트색상}}>이텍스트는크기와색상을변경할수있습니다.</p><div><buttononClick={()=>set텍스트크기(텍스트크기+1)}>텍스트크게</button><buttononClick={()=>set텍스트크기(텍스트크기-1)}>텍스트작게</button></div><div><buttononClick={()=>색상변경('red')}style={{color:'red'}}>빨강</button><buttononClick={()=>색상변경('blue')}style={{color:'blue'}}>파랑</button></div><buttononClick={기본설정적용}>기본설정으로</button></div>);}
useCallback이란?
useCallback은 함수를 “기억"해두는 React의 특별한 기능입니다. 컴포넌트가 다시 그려질 때마다 함수를 새로 만들지 않고, 특정 값이 바뀔 때만 함수를 새로 만들어 성능을 개선합니다.
기본 구조
1
2
3
4
5
6
const기억된함수=useCallback(()=>{// 함수 내용
},[의존성1,의존성2]// 의존성 배열
);
작동 방식
빈 의존성 배열: 기본설정적용 함수는 빈 의존성 배열([])을 사용하여 컴포넌트가 처음 나타날 때 한 번만 만들어지고, 이후에는 계속 같은 함수를 재사용합니다. 이 함수는 항상 같은 일을 수행하므로 다시 만들 필요가 없습니다.
의존성이 있는 경우: 색상변경 함수는 텍스트크기에 의존합니다. 텍스트 크기가 바뀔 때마다 함수도 새로 만들어져야 최신 텍스트 크기 값을 제대로 사용할 수 있습니다. 만약 의존성 배열에 텍스트크기를 넣지 않으면, 함수는 처음에 기억한 이전 값을 계속 사용하게 됩니다.
이렇게 useCallback을 활용하면, 특히 자식 컴포넌트에 함수를 전달할 때 불필요한 재렌더링을 방지하여 앱 성능을 높일 수 있습니다.
importReact,{useState,useCallback}from'react';// 자식 컴포넌트 (React.memo로 최적화)
constTodoItem=React.memo(({todo,onToggle})=>{return(<li><inputtype="checkbox"checked={todo.completed}onChange={()=>onToggle(todo.id)}/>{todo.text}</li>);});// 부모 컴포넌트
functionTodoList(){const[todos,setTodos]=useState([{id:1,text:'리액트 공부하기',completed:false},{id:2,text:'useCallback 이해하기',completed:false}]);// useCallback을 사용하여 함수 메모이제이션
consthandleToggleOptimized=useCallback((id)=>{setTodos(prevTodos=>prevTodos.map(todo=>todo.id===id?{...todo,completed:!todo.completed}:todo));},[]);// 빈 의존성 배열: 함수가 항상 같게 유지됨
return(<div><ul>{todos.map(todo=>(<TodoItemkey={todo.id}todo={todo}onToggle={handleToggleOptimized}//최적화된함수사용/>))}</ul></div>);}
문제와 해결책
문제:
리액트에서 컴포넌트가 다시 그려질 때마다 내부 함수도 새로 만들어집니다. 자바스크립트에서는 함수 내용이 같아도 각각 다른 함수로 취급하기 때문에, 자식 컴포넌트에 props로 전달할 때 문제가 생깁니다.
1
2
3
4
// 두 함수는 같은 일을 하지만 서로 다른 함수로 인식됩니다
constfn1=()=>console.log('안녕');constfn2=()=>console.log('안녕');console.log(fn1===fn2);// false
이 때문에 React.memo로 최적화한 자식 컴포넌트도 불필요하게 리렌더링됩니다. 부모가 리렌더링될 때마다 자식에게 전달되는 함수의 참조값이 매번 바뀌기 때문입니다.
해결책:useCallback은 의존성 배열이 바뀌지 않는 한 같은 함수를 계속 사용합니다.
1
2
3
4
// 메모이제이션된 함수 - 컴포넌트가 리렌더링되어도 같은 참조 유지
consthandleToggle=useCallback((id)=>{// 함수 내용
},[]);
functionWarningExampleComponent(){const[count,setCount]=useState(0);// 1. 불필요한 사용 (과도한 최적화의 예)
constsimpleHandler=useCallback(()=>{console.log('간단한 핸들러');},[]);// 단순한 함수에는 useCallback이 과도할 수 있음
// 2. 의존성 배열 오류 (잘못된 사용)
constwrongDependencies=useCallback(()=>{console.log(`현재 카운트: ${count}`);// count 사용
},[]);// 버그: count가 의존성에서 누락됨
// 3. 올바른 사용 예제
constcorrectExample=useCallback(()=>{console.log(`현재 카운트: ${count}`);console.log(`사용자 이름: ${user.name}`);},[count,user.name]);// 올바른 의존성: 사용하는 모든 값 포함
// 4. 함수형 업데이트로 의존성 줄이기
constincrementWithoutDeps=useCallback(()=>{setCount(c=>c+1);// 이전 상태를 기반으로 업데이트
},[]);// count가 의존성에 필요하지 않음
}
useCallback 사용 시 주요 가이드라인:
성능 측정 후 최적화하기: React DevTools Profiler로 실제 성능 문제를 먼저 확인하세요.
의존성 배열 제대로 관리하기: 함수 안에서 사용하는 모든 값을 의존성 배열에 포함시키세요.
함수형 업데이트 활용하기: 이전 상태를 기반으로 업데이트하면 의존성 배열에서 해당 상태를 제거할 수 있습니다.
importReact,{useState,useCallback,useEffect}from'react';functionProductSearch({categoryId}){const[products,setProducts]=useState([]);const[searchTerm,setSearchTerm]=useState('');const[isLoading,setIsLoading]=useState(false);// API 호출 함수를 useCallback으로 메모이제이션
constfetchProducts=useCallback(async()=>{setIsLoading(true);try{constresponse=awaitfetch(`/api/products?category=${categoryId}&search=${searchTerm}`);constdata=awaitresponse.json();setProducts(data);}catch(error){console.error('상품을 불러오는 중 오류 발생:',error);}finally{setIsLoading(false);}},[categoryId,searchTerm]);// categoryId나 searchTerm이 변경될 때만 함수 재생성
// 컴포넌트 마운트 시 또는 categoryId 변경 시 상품 불러오기
useEffect(()=>{fetchProducts();},[fetchProducts]);// useCallback으로 메모이제이션된 함수를 의존성으로 사용
return(<div>{/* UI 컴포넌트 */}</div>);}
importReact,{useState,useCallback}from'react';// 자식 컴포넌트 (React.memo로 최적화)
constButton=React.memo(({onClick,label})=>{console.log(`${label} 버튼 렌더링됨`);return<buttononClick={onClick}>{label}</button>;});functionCounter(){const[count,setCount]=useState(0);const[theme,setTheme]=useState('light');// 방법 1
constincrementWithEmptyDeps=useCallback(()=>{setCount(count+1);},[]);// 방법 2
constincrementWithDeps=useCallback(()=>{setCount(count+1);},[count]);// 방법 3
constincrementWithFunctionalUpdate=useCallback(()=>{setCount(prevCount=>prevCount+1);},[]);return(<divstyle={{background:theme==='dark'?'#333':'#fff'}}><p>카운트:{count}</p><ButtononClick={incrementWithDeps}label="증가"/><buttononClick={()=>setTheme(theme==='light'?'dark':'light')}>테마변경</button></div>);}
위 코드에는 세 가지 방법으로 구현된 카운트 증가 함수가 있습니다. 이 중에서 불필요한 리렌더링을 막으면서도 정확하게 작동하는 최적의 방법은 무엇일까요? 자신의 생각을 아래에 적어보세요.
정답 확인하기
정답: 방법 3
이 문제는 useCallback을 올바르게 사용하고 의존성 배열을 효과적으로 관리하는 방법을 이해하고 있는지 확인하는 문제입니다.
방법 1: 빈 의존성 배열 사용 빈 의존성 배열 []을 사용하면 함수는 컴포넌트가 처음 마운트될 때 한 번만 생성됩니다. 하지만 함수 내부에서 참조하는 count 값이 업데이트되지 않는 문제가 있습니다. 이로 인해 버튼을 여러 번 클릭해도 카운터가 항상 1만 증가하는 버그가 발생합니다.
방법 2: count를 의존성 배열에 포함 count를 의존성 배열에 포함시키면 최신 count 값을 항상 참조할 수 있어 정확하게 작동합니다. 하지만 count가 변경될 때마다 함수가 새로 생성되기 때문에, Button 컴포넌트가 불필요하게 다시 렌더링됩니다. 이는 useCallback을 사용하는 성능 최적화 효과를 반감시킵니다.
방법 3: 함수형 업데이트 사용 함수형 업데이트 방식(prevCount => prevCount + 1)을 사용하면 함수 내부에서 count 값을 직접 참조하지 않고도 항상 최신 상태값을 기반으로 업데이트할 수 있습니다. 따라서 의존성 배열을 비워도 안전하게 작동하며, 컴포넌트가 리렌더링되어도 함수의 참조가 유지되어 Button 컴포넌트의 불필요한 리렌더링을 방지할 수 있습니다.
이처럼 useCallback과 함수형 업데이트를 함께 사용하면, 불필요한 리렌더링을 방지하면서도 항상 최신 상태를 기반으로 정확하게 동작하는 최적의 방법을 구현할 수 있습니다.