React에서 useState의 인자로 함수 반환값 대신 함수를 넘겨준다면?
React에서 useState의 인자로 함수 반환값을 사용할 때 생길 수 있는 퍼포먼스 이슈
React에서 useState 훅을 사용하여 상태를 관리할 때, 대부분은 단순한 primitive 값을 초기값으로 설정한다. 그러나 때로는 복잡한 연산이 필요한 경우도 있다. 예를 들어, 100개의 랜덤 한 Todo 아이템을 초기값으로 설정해야 한다고 가정해 보자. 일반적으로는 다음과 같은 방식으로 이를 해결한다.
function createInitialTodoList() {
const ret = [];
for (let i = 0 ; i < 100 ; ++i) {
ret.push({
id: crypto.randomUUID(),
text: `todo Item #${i + 1}`,
});
}
return ret;
}
function TotoList() {
const [todoList, setTodoList] = useState(createInitialTodoList());
// ...
}
처음 이 코드를 보면 아무런 문제가 없어 보일 수 있다. 초기 렌더링 시, createInitialTodoList 함수가 실행되어 Todo 리스트를 생성하고 이를 useState 훅의 초기값으로 설정하기 때문에, 기능적으로 잘 동작하는 것처럼 보인다.
그러나 이 접근 방식은 심각한 퍼포먼스 이슈를 일으킬 수 있다. 왜 그럴까? 바로 TodoList 컴포넌트가 다시 렌더링될 때마다 createInitialTodoList 함수가 불필요하게 호출되기 때문이다. 이 함수는 컴포넌트가 마운트 될 때만 실행되어야 하지만, 매번 리렌더링 될 때마다 호출되어 비효율적이다.
이러한 불필요한 연산이 반복되면, 특히 createInitialTodoList 함수가 고비용 연산을 포함하고 있을 경우, 심각한 퍼포먼스 저하로 이어질 수 있다. 결국, 앱의 응답 속도가 느려지고, 전반적인 사용자 경험이 저하될 수 있다.
다음은 createInitialTodoList 함수에 고비용 연산이 포함된 경우, 얼마나 퍼포먼스가 저하될 수 있는지를 보여주는 예제이다. input 창에 텍스트를 쳐보면 눈에 띄게 반응 속도가 느린 것을 확인할 수 있다.
See the Pen Untitled by Ohnim (@ohnim) on CodePen.
이제, 이러한 문제를 React에서 어떻게 해결할 수 있는지 알아보자.
React에서 useState의 인자로 함수 자체를 넘겨주자
앞서 설명한 문제가 복잡해 보일 수 있지만, 해결 방법은 의외로 간단하다. 불필요한 함수 호출을 방지하려면 useState의 인자로 함수의 반환값이 아닌, 함수 그 자체를 넘겨주기만 하면 된다.
function createInitialTodoList() {
// ...
}
function TotoList() {
const [todoList, setTodoList] = useState(createInitialTodoList);
// ...
}
이처럼 useState의 인자로 createInitialTodoList 함수를 넘겨주면, React는 이 함수를 초기 렌더링 시 딱 한 번만 호출한다. 따라서 useState의 인자로 넘겨주는 함수 안에 고비용 연산이 포함되어 있더라도, 초기 렌더링 시에만 그 비용이 발생한다. 이후에는 이 함수가 다시 호출되지 않으므로, 렌더링 성능이 크게 개선된다.
다음은 이 방법을 적용한 예제이다. 간단한 수정이 얼마나 큰 퍼포먼스 개선을 가져올 수 있는지 텍스트 입력을 해 보면서 테스트 해보자.
See the Pen React에서 useState의 인자로 함수 반환값을 사용할 때 생길 수 있는 퍼포먼스 이슈 by Ohnim (@ohnim) on CodePen.
React에서 useState의 인자로 넘겨줄 함수에 파라미터를 받고 싶을 때
실무에서 작업을 하다 보면 useState의 인자로 넘겨줄 함수를 단독으로 사용하는 것보다, 외부 파라미터에 의존하는 함수가 필요할 때가 많다. 이럴 때는 함수를 반환하는 함수를 사용해 문제를 해결할 수 있다.
function todoListInitializerGenerator(todos) {
return function() {
return todos.map((todo) => ({
id: todo.id,
text: `${todo.text} (${todo.completed ? 'V' : ' '})`,
}));
}
}
function TotoList(props) {
const [todoList, setTodoList] = useState(todoListInitializerGenerator(props.todos));
// ...
}
이 코드에서는 todoListInitializerGenerator라는 함수가 외부 파라미터 todos에 의존하여 내부적으로 초기 상태를 반환하는 함수를 생성한다. 이렇게 생성된 함수는 useState에 인자로 전달된다.
처음 문제를 제기할 때 설명한 것처럼, todoListInitializerGenerator 함수는 컴포넌트가 다시 렌더링 될 때마다 실행된다. 하지만, 이 함수는 단지 새로운 함수를 반환할 뿐, 실제 초기값을 만드는 함수는 오직 한 번만 실행된다. 이를 통해 외부 파라미터에 의존하면서도, 고비용 연산이 포함된 초기화 함수가 컴포넌트가 리렌더링 될 때마다 불필요하게 실행되는 것을 방지할 수 있다.
다음은 이 방법을 간단히 테스트해볼 수 있는 예제 코드이다.
See the Pen React에서 useState의 인자로 넘겨주는 함수가 외부 파라미터에 의존하는 예제 by Ohnim (@ohnim) on CodePen.
결론
React에서 useState를 사용할 때, 함수의 반환값 대신 함수 자체를 인자로 넘겨주는 것만으로도 성능 저하를 방지할 수 있다. 이를 통해 복잡한 초기 상태를 효율적으로 설정하면서도, 불필요한 연산을 최소화할 수 있다.
누군가는 이 글을 읽고, 애플리케이션 성능을 높이는데 도움이 되길 바란다.