최적화

ANN·2026년 1월 28일

OneBiteReact

목록 보기
7/8
post-thumbnail

📌 최적화란?

웹 서비스의 성능을 개선하는 모든 행위

아주 단순한 것부터 아주 어려운 방법까지 매우 다양함

일반적인 웹 서비스 최적화 방법

  • 서버의 응답 속도 개선
  • 이미지, 폰트, 코드 파일 등의 정적 파일 로딩 개선
  • 불필요한 네트워크 요청 줄임

리액트 앱 내부의 최적화 방법

  • 컴포넌트 내부의 불필요한 연산 방지
  • 컴포넌트 내부의 불필요한 함수 재생성 방지
  • 컴포넌트 내부의 불필요한 리렌더링 방지

📌 useMemo와 연산 최적화

☑️ useMemo

"메모이제이션" 기법을 기반으로 불필요한 연산을 최적화하는 리액트 룩
자매품: useCallback

📝 메모이제이션
기억해두기, 메모해두기라는 뜻

위와 같이 "반복적으로 수행하는 동일한 연산"을 매번 새롭게 하는 대신

최초로 한 번 계산했을 때의 결과값을 메모리 어딘가에 보관해둔 다음,
다시 이 연산이 필요해지면 저장된 결과값을 바로 돌려주는 기법
➡️ 최초로 한 번 연산을 수행해서 결과값을 저장해둔 후에는
매번 똑같은 연산을 불필요하게 다시 수행할 필요가 없기 때문에
프로그래밍 성능 저하를 방지

일상에서도,...
레스토랑에서 시킨 메뉴가 독특할 때,
지나가는 사람이 그 메뉴가 무엇인지 물어보면...
그때마다 메뉴판을 열어 한참 찾아보고 대답 해주지 않고,
머릿속에 있는 메뉴의 이름을 기억했다가 말해주는 것
➡️ 메모이제이션

위와 같은 방식을 사용하기 위해 useMemo를 사용하면,
특정 연산의 결과 값 기억 가능

☑️ 실습

위 리스트 컴포넌트 안의 todo의 상태를 분석해서 수치로 제공하는 함수 만들기


새로운 함수를 만들어,

  • 전체 todo 아이템의 개수를 저장하는 totalCount 저장
    ➡️ 따라서 todo의 length로 초기화
  • 전체 todo 아이템 중 완료된 todo의 개수 doneCount 저장
    ➡️ filter 메서드를 통해 isDone 이라는 프로퍼티가 참인 아이템만 필터링해서 length로 그 길이 저장
  • 또 완료되지 않은 todo의 개수는 totalCount에서 doneCount만큼 뺀 값 저장

위 세 개의 값을 return 객체로 묶어 내보냄


이제 리스트 컴포넌트가 리렌더링될 때마다
방금 만든 함수를 호출해서 구조분해 할당을 이용해 값을 받아옴


수치 값을 렌더링


그런데 이 연산 과정은 filter라는 배열 메서드를 이용하고 있기 때문에
(필터 메서드는 배열 내에서 전체 요소를 한 번씩 다 순회하기 때문에)

todo state에 보관된 데이터의 개수가 증가하면 증가할수록 더 오래 걸리는 함수가 됨

이 함수가 불필요하게 호출되는 경우를 방지해야 하는데,
이 함수는 컴포넌트 내에서 바로 호출되고 있기 때문에
결국 이 리스트 컴포넌트가 리렌더링될 때마다 새로 호출됨

실제로 확인

콘솔로그를 출력하고 직접 확인해보면


서치바에 값을 입력만 해도,
getAnalyzedData 함수가 호출되고 있음
➡️ 명백한 낭비!

(실제로 어떤 todo 데이터가 추가되거나 삭제되거나 완료되어서 수치 결과가 변하는 것도 아닌데!)
이 서치바에 뭔가를 검색한다고 해서,
이 함수를 다시 호출할 필요가 전혀 없음

물론 새 todo가 추가되거나 수정되거나 삭제되면 호출 되는 게 맞음

결론적으로
연산 자체를 메모이제이션 할 수 있는 방법이 필요하고,
이럴 때 사용하는 게 useMemo라는 훅

➡️ 특정 조건이 만족했을 떄 결과 값을 다시 계산하도록 설정 가능


import를 추가하고,
컴포넌트 내부에서 useMemo 호출

  • 첫 번째 인수: 콜백함수
  • 두 번째 인수: 의존성 배열

이전에 배운 useEffect에서 deps(의존성 배열)에 들어가는 값이 바뀌면
콜백함수를 다시 실행하는 것과 비슷한 매커니즘
useMemo도 똑같이 deps에 포함된 값이 변경되었을 때에만 첫 번째 인수로 전달한 콜백함수 반환
추가로, 해당 콜백 함수가 반환하는 값을 useMemo는 그대로 반환

예를 들어 위 결과값을 받아 사용까지 가능


그렇기 때문에,
useMemo의 첫 번째 인수인 콜백 함수에는 메모이제이션하고 싶은 연산 복붙

위 부분을 복붙해서,

useMemo의 첫 번째 인수에 넣음

첫 번째 인수로 전달한 콜백함수가 반환하는 값을 그대로 반환

그대로 받아오기도 가능,
의존성 배열에 todo를 입력하여,
todo에 의존하도록...

왜냐하면 빈 배열로 두면,
(컴포넌트가 렌더링될 때만, 첫 번째 인수에 들어간 콜백 함수의 연산 수행과 반환이 이루어짐)
todo state의 값이 변경되어도,
새로운 todo가 추가/삭제/수정되어도
이 연산은 다시 실행되지 않음

useMemo가 deps에 의존하는 값이 없어서,
체크를 더 해도, 수치가 갱신되지 않음
➡️ 오류!!🤬


원하는 건!
서치 바에 입력이 되었을 땐 수행하지 않고,
todo 데이터가 추가되었을 땐 수행하긴 해야 함
(todo 데이터를 분석하는 함수니까)

따라서 의존성 배열에 todo를 추가해야 함

잘 갱신되고,


서치 바 업데이트로는
useMemo 내의 연산을 다시 하지 않는 것을 볼 수 있음!


📌 React.memo와 컴포넌트 렌더링 최적화

컴포넌트를 인수로 받아,
최적화된 컴포넌트로 만들어 반환

const MemoizedComponent = memo(Component);

이건 리액트의 내장 메서드

리액트의 컴포넌트를 인수로 받아서,
해당 컴포넌트에 최적화 기능을 추가한 다음
결과값으로 반환


이렇게 최적화 기능이 추가된 컴포넌트는 props를 기준으로 메모이제이션됨

이 MemoizedComponent는 부모 컴포넌트가 리렌더링 되더라도,
자신이 받는 props가 바뀌지 않으면,

다시는 리렌더링이 발생하지 않도록 메모이제이션
➡️ 불필요한 리렌더링이 방지되어 최적화가 이루어짐

☑️ 실습

Header 컴포넌트 최적화

체크박스를 누르면,
Header, Editor, List 컴포넌트까지 모두 리렌더링 발생

todo 데이터 하나 눌렀을 뿐인데,
화면의 모든 컴포넌트가 다시 리렌더링되고 있음

코드 상에서
Header 컴포넌트가 App 컴포넌트의 자식으로 배치되어 있고,
부모 컴포넌트인 App 컴포넌트의 리렌더링에 따라 똑같이 렌더링되고 있음


굳이, 날짜를 렌더링하는 Header 컴포넌트가 리렌더링될 필요가 없음

Header는 props로 받는 값도 없고,...

따라서 이러한 Header 컴포넌트의 불필요한 리렌더링을 방지하기 위해
React의 memo라는 메서드를 사용해보자


  • import 추가
  • 컴포넌트 바깥에서 메모 메서드를 호출하고
    인수로 최적화하고 싶은 컴포넌트인 Header를 그대로 넣음

이제 memo 메서드는 인수로 받은 Header 컴포넌트를 props가 변경되지 않았을 때에는
리렌더링하지 않도록 최적화해서 반환해야 함

위와 같이 변수에 저장해서 export문에서 내보내면 됨


경고가 뜨긴 하는데,
리액트의 동작에는 영향을 주지 않음

해당 옵션을 꺼버리면 됨


이렇게 React의 memo 메서드를 사용해서 컴포넌트를 최적화하면,
내보낸 memoizedHeader 컴포넌트는 자신이 받는 props가 바뀌지 않으면
다시는 리렌더링되지 않음


위처럼 단축해서 export해도 됨

출근하기 todo를 수정하는데,
굳이 다른 아이템 모두 리렌더링이 되고 있음
➡️ 엄연히 불필요한 리렌더링

todo 아이템 컴포넌트들도 props가 변경되지 않으면 리렌더링되지 않도록
메모 메서드를 이용해서 최적화


위와 같이 memo 메서드를 이용해서 최적화


그럼 이제 TodoItem 컴포넌트는,
위 props가 바뀌지 않는 이상
렌더링이 발생하지 않을 것


그럼 이제 다시 체크 박스를 눌러가며,
렌더링 되는 걸 보면...

응 여전함

memo 메서드가 제대로 동작하지 않음

분명 memo 메서드를 이용해서,
TodoItem 컴포넌트의 props가 바뀌지 않을 경우
리렌더링 되지 않도록 설정했는데,
왜 첫 번째 todo 아이템을 수정하는데, 두 번째 세 번째 아이템 컴포넌트도 리렌더링 되는 것인가?

왜냐하면,
이 체크박스를 클릭해서 todo 아이템을 변경하면,
App 컴포넌트에 있는 todo state의 값을 바꾸게 되면
App 컴포넌트가 리렌더링 됨
➡️ App 컴포넌트가 다시 호출됨

따라서 위 onCreate, onUpdate 함수도 새로 만들어짐

함수는 객체 타입
새롭게 생성된 위 함수들이 같은 동작을 하더라도,
새롭게 생성될 때마다 다른 값으로 인식됨

함수는 객체 타입에 해당하므로,
변수에 주소값이 저장되는데,
객체 간의 비교는 이 주소값을 기반으로 수행됨

위 두 개의 객체는 같아 보이지만, ===로 비교하면, 다른 객체라고 평가됨

따라서 객체 타입에 해당하는 함수 또한 컴포넌트가 리렌더링 되면서
새롭게 다시 생성되면
주소값이 계속 바뀌기 때문에
사실상 매번 다른 값으로 생성되는 것으로 판단

➡️ 즉, TodoItem 컴포넌트에게 전달되는 onUpdate, onDelete가
매번 App 컴포넌트가 리렌더링될 때마다
매번 새롭게 생성이 되어서 전달이 되고 있던 것임

memo 메서드는 props가 바뀌었을 때만
컴포넌트를 리렌더링하도록 최적화하기 때문에,

매번 리렌더링될 때마다
현재의 props와 과거의 props를 비교
➡️ 두 개의 props가 같은 값인지 다른 값인지 판단해서,
TodoItem 컴포넌트를 리렌더링 할지말지 결정

이 memo 메서드는 얕은 비교로 비교하기 때문에,
객체 타입의 값은 무조건 서로 다른 값이라고 판단되는 것

➡️ props가 바뀐다고 판단되기 때문에,

결과적으로 브라우저에서 하나의 todo 아이템을 수정하게 되면
다른 todo 아이템들에서도 결국 모두 리렌더링이 발생하는 것


이렇게 객체 타입의 값을 props로 받고 있는 컴포넌트를 예를 들어보면,
memo 메서드를 적용한다기만 한다고 해서 최적화가 제대로 이루어지지 않음

App 컴포넌트에서 이 함수 자체를 메모이제이션해서,
리렌더링이 되더라도 다시 생성되지 않게 방지하려면, useCallback을 이용해야 함


TodoItem의 memo 메서드 안에,
두 번재 인수로 콜백함수를 추가로 전달해서
최적화 기능을 커스터마이징

메모 함수의 두 번재 인수로 콜백 함수를 전달할 수 있는데,
이 콜백 함수는 보통 생략하지만
전달하게 되면,

memo 메서드는 부모 컴포넌트가 리렌더링될 때마다,
컴포넌트의 props를 바뀌었는지 아닌지 스스로 판단하는 대신,
➡️ 콜백함수의 매개변수로 과거의 props인 prevProps와 미래의 props인 nextProps를 전달해서,
이 함수의 반환값에 따라 props가 바뀌었는지 안 바뀌었는지 판단

콜백함수에서

  • true를 반환하면 Props가 바뀌지 않았다고 판단
  • false를 반환하면 Props가 바뀌었다고 판단

TodoItem 컴포넌트가 받는
컴포넌트의 props 중에 onUpdate, onDelete 빼고
그 외의 props가 바뀌었을 때만 리렌더링을 시켜주자

네 개의 값이 바뀌지 않았다면 true가 반환해서 리렌더링하지 말라고 설정


이제 해당 컴포넌트만 리렌더링이 되는 걸 볼 수 있음


컴포넌트를 인수로 받아서 해당 컴포넌트에 최적화나,
메모이제이션 같은 추가적인 기능을 덧붙여 기능이 추가된
컴포넌트를 반환하는 memo와 같은 메서드를
리액트에서는 고차 함수 컴포넌트라고 함

Higher Order Component = HOC
한 번 호출하는 것만으로도 컴포넌트에 새로운 기능 부여 가능
➡️ 복잡한 리액트 앱을 구축할 때 꽤나 자주 쓰는 방식


📌 useCallback과 함수 재생성 방지

memo 메서드는 현재 컴포넌트의 props가 변경되었는지 얕은 비교로 판단하기 때문에
onUpdate나 onDelete 같은 함수,
객체 타입의 값을 props로 전달할 때는 제대로된 최적화가 이루어지지 않아서

별도로 콜백 함수를 추가적으로 전달해서,
일일히 하나하나의 props의 값이 바뀌었는지 비교해야 함

아래처럼...

그런데 매번 이런 식이면 불편함
props의 이름이 바뀌거나 추가되면,
매번 조건을 수정하거나 추가해야 하기 때문

이럴 때는 그냥 onUpdate나, onDelete 같은 함수가
애초에 생성되지 않기 최적화
➡️ useCallback을 사용


App 컴포넌트에,
useCallback 사용

  • 첫 번째 인수로는 최적화하고 싶은 함수
    그러니까 불필요하게 재생성되지 않도록 방지하고 싶은 함수
  • 두 번째 인수로는 의존성 배열

이 useCallback은 기본적으로
우리가 첫 번째 인수로 전달한 콜백함수를 그대로 생성해서 반환
➡️ 따라서 변수에 담을 수 있음

이렇게 생성되는 함수를 deps가 변경되었을 때만 다시 생성
즉, 함수를 렌더링

의존성 배열이 비어있으면,
이 컴포넌트가 최초로 한 번 렌더링 될 때
마운트 될 때에만 이 함수를 생성하고,
그 뒤에는 리렌더링이 발생해도 이 함수를 생성하지 않음
➡️ onDelete, onUpdate 같은 함수를 생성하지 않게 최적화 가능


useCallback을 호출하고,

첫 번째 인수로는 메모이제이션 함수를 넣어야 하니,
onDelete 함수를 세미콜론만 빼고 복사해서 익명 함수로 복사해서 붙여넣기

deps에는 빈 배열을 넣어서 onDelete를 다시는 생성하지 않도록 하기

기존 onDelete 함수는 지워주기

➡️ 이제 onDelete 함수는 마운트 되었을 때 딱 한 번만 생성
이후에는 컴포넌트가 리렌더링 되어도 재생성되지 않도록 최적화됨


세 함수 모두 마운트 이후 생성되지 않도록 최적화 완성


이제 TodoItem 컴포넌트에서 더 이상,
별도의 컴포넌트를 memo 메서드에 전달하지 않아도
props로 받는 onUpdate와 onDelete 함수가 다시 생성되지 않은 채로 받게 될 것

따라서 이전에 했던 것처럼 memo 메서드만 적용한
TodoItem을 내보내도 최적화 가능


이러한 최적화는 언제 하는가?

  • 너무 이른 타이밍에 해도 문제가 되고
  • 너무 많이 최적화해도 문제가 될 수 있음

따라서,
리액트 앱을 최적화할 때는 하나의 프로젝트를 거의 완성한 상태에서 최적화

기능을 구현, 완성 후 최적화 진행

왜냐하면, 이런 useCallback 같은 메서드를 적용하고 나면,
새로운 기능을 덧붙이거나 수정할 때 최적화가 풀리거나 아예 고장남

➡️ 구현이 완료되고 마지막에 최적화 하는 걸 권장


그리고 모든 것에 최적화하면 안 되고,
최적화가 필요할 것 같은 연산이나 함수나 컴포넌트에만 최적화

왜냐하면,
Header 컴포넌트를 최적화한 memo 메서드는 매우 단순해 보이지만
당연히 연산이 필요함

  • props의 값을 비교하거나,
  • 메모이제이션을 위해메모리에 컴포넌트의 결과 값을 보관한다든가

그래서 이렇게 최적화된 컴포넌트가 고작 별거 아닌 UI를 렌더링하는 컴포넌트면
리렌더링이 빠를 수도...


따라서 Header 같은 사소한 컴포넌트는 최적화를 잘 하지 않고...
TodoItem 컴포넌트처럼 유저의 행동에 따라 개수가 많아지는 컴포넌트나,
함수를 많이 가지고 있어서 코드가 무거운 컴포넌트에 한해
최적화를 수행하는 것을 권장

아티클 "When to use useMemo, useCallback"
https://goongoguma.github.io/2021/04/26/When-to-useMemo-and-useCallback/

0개의 댓글