일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 삼항연산자
- RN 프로젝트
- 프로그래머스
- 자바스크립트 sort()
- HTML
- 자바스크립트 reduce()
- 엘리스 ai 트랙
- 프론트개발
- 리트코드
- [파이썬 실습] 기초 문제
- 자바스크립트 split()
- [AI 5기] 연습 문제집
- JavaScript
- 자바스크립트 날씨
- [파이썬 실습] 심화 문제
- 날씨 웹 만들기
- 자바스크립트 날씨 웹 만들기
- 엘리스
- 프론트개발공부
- 개발공부
- reactnativecli
- 코딩부트캠프
- 부트캠프
- leetcode
- [파이썬 실습] 중급 문제
- 간단한 날씨 웹 만들기
- 자바스크립트
- 엘리스 AI 트랙 5기
- 개발일기
- 코드스테이츠
- Today
- Total
개발조각
[React] React.memo 본문
컴포넌트는 왜 계속 렌더링이 되는 거지? 불필요한 렌더링을 막을 방법이 없을까?
리액트를 개발하다보면 계속해서 렌더링 할 필요 없는 컴포넌트가 계속 반복해서 렌더링이 되는데
해당 컴포넌트가 렌더링 될때마다 무거운 로직을 수행한다면 컴포넌트의 성능은 안 좋아진다.
React.memo 개념 설명
일반적으로 리액트는 성능이 굉장히 좋기 때문에 대부분일 경우에는 컴포넌트 렌더링 횟수에 대해서 크게 고민할 필요가 없다.
하지만 렌더링이 지나치게 많이 발생하거나 렌더링 될 때마다 무거운 로직을 처리해서 성능의 부담을 준다면 불필요한 렌더링은 최대한 막아야 된다.
// 부모 컴포넌트
const School = (props) => {
return (
<Student
name={'홍길동'}
age={20}
address={'우리집'}
/>
);
}
// 자식 컴포넌트
const Student = ({name, age, address}) => {
return (
<div>
<h1>{name}</h1>
<span>{age}</span>
<span>{address}</span>
</div>
)
}
리액트에서는 기본적으로 부모 컴포넌트가 렌더링이 되면 모든 자식 컴포넌트들도 자동적으로 렌더링이 된다.
여기서는 School컴포넌트가 렌더링이 되면 Student컴포넌트도 같이 렌더링이 된다.
Student컴포넌트는 항상 같은(고정적인) porps를 전달받기 때문에(props가 바뀌지 않기 때문에) Student는 항상 같은 결과 같은 결과를 화면에 보이게 된다.
만약 부모컴포넌트인 School이 정말 자주 바뀌는 컴포넌트라면 자식컴포넌트 Studemt는 늘 같은 결과를 보여주는데도 불구하고 부모 컴포넌트와 함께 자주 렌더링이 된다.
Student의 props(name, age, address) 변경이 될 때만 렌더링이 되게 만든다면 훨씬 효율적이지 않을까?
React.memo를 사용하면 된다.
React.memo
React.memo는 리액트에서 사용하는 고차컴포넌트(HOC)이다.
보통 컴포넌트 → React.memo (HOC) → 최적화된 컴포넌트
고차컴포넌트란 어떤 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환해 주는 함수
React.memo라는 고차 컴포넌트를 넣어주면 UI적으로나 기능적으로는 똑같지만 조금 더 최적화된 컴포넌트를 반환해 준다.
<Student /> → React.memo → <Student /> PropCheck
최적화된 컴포넌트는 렌더링 해야 되는 상황에 놓일 때마다 Prop Check를 통해
자신이 받는 props가 변화가 있는지 없는지 확인하게 된다.
확인을 해서 props의 변화가 있으면 렌더링을 하고 props의 변화가 없다면 렌더링 하는 게 아니라 기존에 이미 렌더링 된 내용을 재사용한다.
// 부모 컴포넌트
const School = (props) => {
return (
<Student
name={'홍길동'}
age={20}
address={'우리집'}
/>
);
}
// 자식 컴포넌트
const Student = ({name, age, address}) => {
return (
<div>
<h1>{name}</h1>
<span>{age}</span>
<span>{address}</span>
</div>
)
}
앞에 School, Student 예제에서 Student 컴포넌트가 React.memo를 통해서 최적화가 되어 있다면
Student 컴포넌트는 Prop Check! 를 하게 된다.
Student가 받을 Prop Check는
Prop Check!
렌더링 O
- name, age, address 중 변화가 있다.
렌더링 X → 재사용 O
- name, age, address가 변하지 않았다.
- 이미 렌더링 된 결과를 재사용하는 최적화된 컴포넌트가 되는 것이다.
React.memo
React.memo에서 memo는 Memoization을 의미한다.
메모이제이션이란 이미 계산한 값을 메모리에 저장해 놓고 필요할 때마다 꺼내서 재사용하는 기법이다.
React.memo를 잘 사용한다면 렌더링 횟수 감소로 인해 이득을 볼 수 있지만
무분별하게 사용한다면 오히려 성능에 독이 될 수 있다.
왜냐하면 컴포넌트를 메모이징 할 때 렌더링된 결과를 어딘가에 저장해야 되기 때문에 메모리를 추가적으로 소비한다.
React.memo 꼭 필요할 때만!
1) 컴포넌트가 같은 Props로 자주 렌더링 될 때
2) 컴포넌트가 렌더링이 될 때마다 복잡한 로직을 처리해야 한다면
이외의 경우에는 React.memo를 사용해야 되는지 10번 정도 생각해 보자
React.memo는 오직 Props 변화에만 의존하는 최적화 방법입니다!
만약 컴포넌트가 useState, useReducer, useContext와 같이 상태와 관련된 훅을 사용한다면
prop의 변화가 없더라도 여전히 state, context가 변화하면 다시 렌더링 한다는 것을 기억해야 된다.
예제 1
const ReactMemo1 = () => {
const [parentAge, setParentAge] = useState(0);
const [childAge, setChildAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
const incrementChildAge = () => {
setChildAge(childAge + 1);
};
console.log("👨👩👧 부모 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid navy", padding: "10px" }}>
<h1>👨👩👧 부모</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<button onClick={incrementChildAge}>자녀 나이 증가</button>
<Child name={"홍길동"} age={childAge} />
</div>
);
};
const Child = ({ name, age }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>name: {name}</p>
<p>age: {age}살</p>
</div>
);
};
리액트는 자신의 State가 업데이트가 될 때마다 렌더링이 된다.
그래서 parentAge, childAge가 업데이트될 때마다 ReactMemo1은 렌더링이 되고
이 컴포넌트가 가진 모든 자식 컴포넌트도 함께 렌더링이 된다.
부모 나이 증가 버튼을 클릭하면 parentAge state가 업데이트가 되기 때문에 콘솔에 출력된다.
자녀 나이 증가 버튼을 클릭하면 childAge state 업데이트가 되기 때문에 콘솔에 출력된다.
그러나 부모 나이 증가를 클릭하면 자녀 나이도 증가해야 되는데 증가하지 않는다.
부모의 나이가 증가해도 자녀 나이는 똑같은 모습을 화면에 보여준다.
자녀나이는 업데이트되는 게 없는데 부모나이 증가를 누를 때마다 자녀 컴포넌트를 업데이트시켜야 될까?
만약 Child컴포넌트가 렌더링이 일어날 때마다 복잡한 작업을 수행을 한다면 성능이 나빠질 것이다.
Child컴포넌트는 prop으로 받은 name, age가 업데이트가 되지 않는 이상 렌더링이 될 필요가 없는 컴포넌트이다.
리액트에서 제공해 주는 React.memo를 통해 Child컴포넌트 props가 업데이트되지 않았다면 렌더링을 막아주는 최적화를 시켜줄 수 있다.
React.memo 사용법
최적화하고 싶은 컴포넌트를 memo라는 함수로 감싸주면 된다.
그냥 적용
const Child = memo(({ name, age }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>name: {name}</p>
<p>age: {age}살</p>
</div>
);
});
내보내기 시 적용
const Child = ({ name, age }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>name: {name}</p>
<p>age: {age}살</p>
</div>
);
};
export default memo(Child);
부모 나이 증가를 클릭하면 "부모 컴포넌트 렌더링이 되었어요."만 출력된다.
=> 부모 컴포넌트는 렌더링 되는데 자녀 컴포넌트는 렌더링이 되지 않음 -> 받는 prop가 변경되지 않았기 때문
React.memo가 언제 동작이 되는지
React.memo는 리액트에서 제공되는 고차 컴포넌트이다.
고차컴포넌트란 컴포넌트를 인자로 받아서 또 다른 컴포넌트를 반환해 준다.
여기서는 React.memo라는 고차 컴포넌트는 Child컴포넌트를 인자로 받아서 최적화된 Child컴포넌트를 반환하게 해 준다.
export default memo(Child);
React.memo를 통해 최적화된 컴포넌트는 렌더링이 될 상황에 놓일 때마다 prop Check를 하게 된다.
prop Check란 컴포넌트가 받는 props에 변화가 있을 때만 렌더링을 허락해 주고
만약 Props변화가 없다면 렌더링 하지 않고 이전의 컴포넌트 결과를 재사용하는 것이다.
Child컴포넌트 같은 경우에는 React.memo로 최적화되어있기 때문에 props로 받은 name, age의 변화가 없다면 레더링을 하지 않고 name과 age의 변화가 있을때만 렌더링을 다새 해주는 것이다.
- 부모 나이 증가 클릭
- parentAge state 업데이트
- ReactMemo1(부모 컨포넌트) 렌더링
- Child(자식 컴포넌트) 업데이트
- Child(자식 컴포넌트)는 React.memo로 최적화 되어 있음
- Prop Check를 통해 Child(자식 컴포넌트)의 props의 변화가 있는지 체크
- Child(자식 컴포넌트)의 props는 변화가 없기 때문에 이전 렌더링 결과 출력
자세하게 설명하자면 부모 나이 증가를 클릭하면 parentAge state 업데이트되기 때문에 ReactMemo1 컴포넌트는 렌더링이 될 것이다.
이 컴포넌트가 렌더링이 된다는 것은 자녀인 Child컴포넌트도 업데이트가 될 상황에 놓이게 된다.
하지만 Child컴포넌트는 React.memo로 최적화되어있기 때문에 렌더링이 되기 전에 Prop Check를 하게 된다.
그래서 Child가 받는 name, age의 변화가 있는지 없는지 확인하게 된다.
지금은 name, age 변화가 없기 때문에 React.memo는 Child컴포넌트는 변화될 필요가 없고 판단하게 되고 이전에 렌더링 된 결과를 출력하게 되는 거다.
만약 자녀 나이 증가를 클릭하면 Child컴포넌트가 렌더링이 된다.
useMemo와 useCallback을 React.memo와 함께 사용한다면 훨씬 더 많이 적용할 수 있는 폭넓은 최적화를 할 수 있다.
useMemo + React.memo
const ReactMemo2 = () => {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log("👨👩👧 부모 컴포넌트가 렌더링이 되었어요.");
const name = {
lastName: "홍",
firstName: "길동",
};
return (
<div style={{ border: "2px solid navy", padding: "10px" }}>
<h1>👨👩👧 부모</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<Child name={name} />
</div>
);
};
const Child = memo(({ name }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>성: {name.lastName}</p>
<p>이름: {name.firstName}</p>
</div>
);
});
만약 부모 나이 증가를 클릭하면 부모컴포넌트만 렌더링 될 거라고 생각하지만 둘 다 출력이 된다.
React.memo로 자식 컴포넌트인 Child컴포넌트를 최적화했는데 왜 Child컴포넌트도 렌더링이 될까?
전달해 준 name이 오브젝트이기 때문이다.
자바스크립트에서 오브젝트는 스트링, 넘버와 같은 원시타입과는 다른 게 변수 안에 그대로 저장되는게 아니라
오브젝트가 저장된 메모리 주소가 변수안에 저장이 된다.
만약 이 오브젝트가 #1111이라는 메모리 주소에 저장되어 있다면 name안에는 1111이 들어가게 되는 것이다.
컴포넌트가 렌더링 될 때마다 ReactMemo2가 불리게 되는 것이다.
함수가 호출이 된다는 것은 함수 안에 있는 모든 변수들이 다시 초기화가 된다.
이 말은 name도 다시 초기화가 되는 거다.
그래서 ReactMemo2가 렌더링 될 때마다 계속해서 새로운 오브젝트가 만들어지게 되고,
만들어진 오브젝트는 각각의 메모리 주소에 저장이 된다.
그래서 ReactMemo2가 렌더링 될때마다 name안에는 다른 주소가 들어있게 된다.
Child컴포넌트에 name prop으로 전달되는 오브젝트의 내용은 늘 똑같겠지만
전달되는 name변수 안에 있는 메모리 주소 값은 매번 달라지기 때문에
React.memo 입장에서는 "name이라는 prop에 name이 있구나"라고 받아들이게 된다.
그래서 Child 컴포넌트를 최적화되었다고 해도 계속해서 ReactMemo2가 렌더링 될 때마다 Child컴포넌트도 렌더링이 된다.
useMemo훅을 사용해서 오브젝트를 메모이제이 시원해주면 된다.
오브젝트가 할당되어 있는 메모리주소가 변하지 않게 된다.
수정 부분
const name = useMemo(() => {
return {
lastName: "홍",
firstName: "길동",
};
}, []);
전체코드
const ReactMemo2 = () => {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log("👨👩👧 부모 컴포넌트가 렌더링이 되었어요.");
const name = useMemo(() => {
return {
lastName: "홍",
firstName: "길동",
};
}, []);
return (
<div style={{ border: "2px solid navy", padding: "10px" }}>
<h1>👨👩👧 부모</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<Child name={name} />
</div>
);
};
const Child = memo(({ name }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>성: {name.lastName}</p>
<p>이름: {name.firstName}</p>
</div>
);
});
useCallback + React.memo
const ReactMemo3 = () => {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log("👨👩👧 부모 컴포넌트가 렌더링이 되었어요.");
const tellMe = () => {
console.log("길동아 사랑해💕");
};
return (
<div style={{ border: "2px solid navy", padding: "10px" }}>
<h1>👨👩👧 부모</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<Child name={"홍길동"} tellMe={tellMe} />
</div>
);
};
const Child = memo(({ name, tellMe }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>이름: {name}</p>
<button onClick={tellMe}>엄마 나 사랑해?</button>
</div>
);
});
부모나이 증가를 클릭하면 자식컴포넌트도 같이 렌더링 된다.
Child에서 name, tellMe props는 업데이트가 되지 않았다.
Child컴포넌트는 React.memo를 통해서 최적화되어있는데도 불구하고 렌더링이 된다.
이유는 tellMe라는 Prop이 함수를 받고 있기 때문이다.
자바스크립트의 함수는 객체의 한 종류이다.
name prop을 string이 아니라 객체를 전달해 준 것과 같은 이유로
tellMe라는 변수 안에는 함수라는 객체가 들어 있는 메모리 주소가 할당되어 있어서
ReactMemo3이 렌더링 될 때마다 Child컴포넌트 tellMe prop은 계속해서 다른 메모리 주소가 전달된다.
그래서 React.memo는 함수의 내용이 똑같해도 tellMe라는 변수에 들어있는 메모리 주소 변경되어
"Child컴포넌트는 다시 렌더링 해야 되는 군아"라고 생각한다.
이 문제를 해결하려면 useCallback을 사용하면 된다.
useCallback은 useMemo와 비슷한 기능을 하는데
- useMemo는 어떠한 값을 메모이징 하기 위해 사용
- useCallback은 어떠한 함수를 메모이징 하기 위해 사용
한다.
수정 부분
const tellMe = useCallback(() => {
console.log("길동아 사랑해💕");
}, []);
tellMe는 항상 같은 주소를 가지고 있게 된다.
전체 코드
const ReactMemo3 = () => {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log("👨👩👧 부모 컴포넌트가 렌더링이 되었어요.");
const tellMe = useCallback(() => {
console.log("길동아 사랑해💕");
}, []);
return (
<div style={{ border: "2px solid navy", padding: "10px" }}>
<h1>👨👩👧 부모</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<Child name={"홍길동"} tellMe={tellMe} />
</div>
);
};
const Child = memo(({ name, tellMe }) => {
console.log("👶 자녀 컴포넌트가 렌더링이 되었어요.");
return (
<div style={{ border: "2px solid powderblue", padding: "10px" }}>
<h3>👶 자녀</h3>
<p>이름: {name}</p>
<button onClick={tellMe}>엄마 나 사랑해?</button>
</div>
);
});
부모 나이 증가를 클릭하면 부모 컴포넌트만 렌더링 된다.
useCallback과 React.memo를 함께 사용한다면 Child의 prop으로 함수를 전달해 줘도 Child의 컴포넌트의 렌더링을 제한해 줄 수 있다.
※ 이 글은 별코딩 리액트 훅 강의를 보고 정리한 글입니다.
'React' 카테고리의 다른 글
[React] 채팅 데이터로 채팅 스크롤 및 화면 구현 (0) | 2023.12.19 |
---|---|
[React] Custom Hooks (0) | 2023.09.20 |
[React] useReducer (0) | 2023.09.19 |
[React] useCallback (0) | 2023.09.19 |
[React] useMemo (0) | 2023.09.15 |