Algorithm
function solution(cacheSize, cities) {
let answer = 0;
let obj = {}
for(let i=0; i<=cities.length-1;i++) {
if(cacheSize===0) {
answer+=5
continue;
}
let city = cities[i].toLowerCase()
let key = getKeyByValue(obj,city)
if(key!==undefined){
answer += 1
delete(obj[key])
obj[i] = city
continue;
}
if(Object.keys(obj).length===cacheSize) {
let idx = Math.min(...Object.keys(obj)).toString()
delete(obj[idx])
obj[i] = city
answer += 5
continue;
}
obj[i] = city
answer +=5
}
return answer;
}
function getKeyByValue(obj, value) {
return Object.keys(obj).find(key => obj[key] === value);
}
제한사항을 확인해보니 큐를 사용한 풀이법이 좋을 것 같았지만 JS에서 큐를 활용하기 위한 shift
는 시간 복잡도 문제가 있어 사용하지 않고 싶어 객체로 풀었다..
하지만 value
를 통해 key
를 찾아오는 과정에서 find
함수를 쓰다보니 결국 똑같이 시간복잡도가 증가하는 결과가 나왔고 결국 풀긴 했지만 데이터 크기가 커질수록 시간이 오래 걸리는 문제점을 가졌다.
function solution(cacheSize, cities) {
const MISS = 5, HIT = 1;
if (cacheSize === 0) return MISS * cities.length;
let answer = 0,
cache = [];
cities.forEach(city => {
city = city.toUpperCase();
let idx = cache.indexOf(city);
if (idx > -1) {
cache.splice(idx, 1);
answer += HIT;
} else {
if (cache.length >= cacheSize) cache.shift();
answer += MISS;
}
cache.push(city);
});
return answer;
}
위는 내가 원하던 큐로 풀이한 코드로 하나씩 살펴보자면
cacheSize
가 0이라면 모두 미스가 나기때문에 cities
의 크기를 미스 값에 곱한다answer
와 큐로 사용할 cache
배열을 선언cities
를 반복문 돌린 후 일단 대소문자 관계 없이 써진 값들을 toUpperCase()
로 통일시킨다cache
)에 저장된 city
값이 잇는데 찾는다idx > -1
은 있다는 얘기로(indexOf
는 값이 없을 경우 -1을 반환한다) splice
를 통해 해당 city
값이 있는 위치를 잘라낸 후 answer
에 히트 +
shift()
로 배열의 맨 앞쪽을 제거하고 push
로 가장 뒤에 넣어준다.위 코드와 동일한 방식으로 코드를 짰지만 다른 좋은 방법이 없을까 시도해 보았다. 하지만 캐시 메모리 크기가 30으로 제한되어 있으니 shift를 활용하는 게 훨씬 좋았던 것 같다.
CS
일단 컴포넌트의 리렌더링 되는 조건
props
가 변경될 때state
가 변경 될 때React Hook 중 하나로서 React에서 CPU 소모가 심한 함수들을 캐싱하기 위해 사용
만약 텀포넌트 내의 어떤 함수가 값을 리턴하는데 하나의 변화에도 값을 리턴하는데 많은 시간을 소요한다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 된다.
또 함수가 return 되는 값이 자식 컴포넌트에도 사용 된다면, 그 자식 컴포넌트도 함수가 호출 될 때마다 새로운 값을 받아 리렌더링 된다.
만약 컴포넌트 내에 어떤 함수가 값을 리턴하는데 많은 시간을 쓴다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출 되면서 많은 시간을 소요할 것이고 함수가 반환하는 값을 하위 컴포넌트가 사용한다면 그 하위 컴포넌트는 매 함수 호출마다 새로운 값을 받아 리렌더링 할 것이다.
useMemo(()=> func, [input_dependency])
func
은 캐시하고 싶은 함수, input_dependency
는 useMemo가 캐시할 func에 대한 입력의 배열로서 해당 값들이 변경되면 func이 호출된다
이것을 적용하면 input_dependency
가 있는 데이터가 변할 때에만 평균을 구하는 연산을 수행
useMemo는 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용한다!
즉, 함수 호출 시간도 세이브 간으하고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지가능
React.memo는 Hook이 아니기 때문에 클래스형 컴포넌트에서 사용 가능하다. 함수형 컴포넌트에서는 shouldComponentUpdate를 사용할 수 없는데, 리액트 공식 문서에서는 그 대안으로 React.memo를 제시하고 있고,
React.memo를 통해 컴포넌트의 props가 바뀌지 않았다면, 리렌더링 하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 해줄 수 있다
export default memo(Item);
이런식으로 가능하다
이럴 경우 만약 List
라는 컴포넌트를 리렌더링 시키더라도 새로 추가된 Item
만 새로 렌더되고 이미 렌더된 Item
들은 리렌더링 되지 않는다
useMemo가 리턴되는 값을 memoize 시켰는데, useMemo와 비슷한 useCallback은 함수 선언을 memoize 하는데 사용된다
예시를 보자면
import React.{memo} from "react";
function Button({ onClick }) {
console.log("Button component render");
return (
<button type="button" onClick={onClick}>
버튼
</button>
);
}
export default memo(Button);
onClick 함수는 UserList
에서 전달 받고 있다고 가정하자
UserList는 input에 타이핑을 할때마다, 자식(Button)트리를 포함하여 리렌더링 된다.
그런데 리렌더링마다 addUser라는 함수를 새로 생성하여 Button 컴포넌트에 props로 전달해주고 있다.
여기서 Button
컴포넌트는 불필요한 렌더링을 막기 위해 memo를 이용하여 memoize되어 있다
React.memo는 현재와 다음 props를 비교하여 이전 props와 같다면 컴포넌트를 리렌더링 하지 않는다.
하지만! 이 경우 Button 컴포넌트도 같이 리렌더링 되는 문제가 발생되는데, 이 상황에선 Button 컴포넌트에 memo로 감싸도 소용이 없다.
그 이유는 함수는 객체이고, 새로 생성된 함수는 다른 참조 값을 가지기 때문에 Button입장에서는 새로 생성된 함수를 받을 때 props가 변한 것으로 인지하기 때문이다
그래서 이럴 때 useCallback을 써야한다.
useCallback으로 함수를 선언해주면, 종속 변수들이 변하지 않는 이상 굳이 함수를 재생성하지 않고 이전에 있떤 참조 변수를 그대로 하위 컴포넌트에 props로 전달하여, 하위 컴포넌트도 props가 변경되지 않았다고 인지하게 되어 하위 컴포넌트의 리렌더링을 방지할 수 있다.
흔히 작업을 하다보면 props의 값으로 객체를 넘겨주는데 이때 props로 전달하는 형태에 주의하여야 한다
// 생성자 함수
<Component prop={new Obj("x")} />
// 객체 리터럴
<Component prop={{property: "x"}} />
이런 경우 새로 생성된 객체가 props로 들어가므로 컴포넌트가 리렌더링 될 때마다 새로운 객체가 생성되어 자식 컴포넌트로 전달된다
props로 전달한 객체가 동일한 값이어도 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문에 자식 컴포넌트는 메모이제이션이 되지 않는다
안좋은 예시
// UserList.jsx
function UserList() {
{...}
const getResult = useCallback((score) => {
if (score <= 70) {
return { grade: "D" };
} else if (score <= 80) {
return { grade: "C" };
} else if (score <= 90) {
return { grade: "B" };
} else {
return { grade: "A" };
}
}, []);
return(
<div>
{users.map((user) => {
return (
<Item key={user.id} user={user} result={getResult(user.score)} />
);
})}
</div>
)
export default memo(UserList);
// Item.jsx
function Item({ user, result }) {
console.log("Item component render");
return (
<div className="item">
<div>이름: {user.name}</div>
<div>나이: {user.age}</div>
<div>점수: {user.score}</div>
<div>등급: {result.grade}</div>
</div>
);
}
export default Item;
이럴 때는 생성자 함수나 객체 리터럴로 객체를 생성하여 하위 컴포넌트로 넘겨주는 방식이 아닌, state를 그대로 하위 컴포넌트에 넘겨주어 필요한 데이터 가공을 하위 컴포넌트에서 해주는 것이 좋다!
// UserList.jsx
function UserList() {
{...}
return(
<div>
{users.map((user) => {
return (
<Item key={user.id} user={user} />
);
})}
</div>
)
export default memo(UserList);
// Item.jsx
function Item({ user }) {
console.log("Item component render");
const getResult = useCallback((score) => {
if (score <= 70) {
return { grade: "D" };
}
if (score <= 80) {
return { grade: "C" };
}
if (score <= 90) {
return { grade: "B" };
} else {
return { grade: "A" };
}
}, []);
const { grade } = getResult(user.score);
return (
<div className="item">
<div>이름: {user.name}</div>
<div>나이: {user.age}</div>
<div>점수: {user.score}</div>
<div>등급: {grade}</div>
</div>
);
}
export default memo(Item);
많이 하는 실수가 컴포넌트를 매핑할 때 key값에 index값을 넣어주는데 이는 최적화에 매우 좋지 않다.
배열 중간에 어떤 요소가 삽입될 때 그 중간 이후에 위치한 요소들은 전부 index가 변경되는데 이로 인해 key값이 변경되어 React는 key가 동일 할 경우, 동일한 DOM Element를 보여주기 때문에 예상치 못한 문제가 발생한다. 또한, 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다.
?? 그러면 index요소는 반드시 사용하면 안될까?
배열의 요소가 필터링, 정렬 삭제, 추가 등의 기능이 들어간다면 문제가 발생할 수 있으나 다음과 같은 경우에서는 index로 사용해도 무방하다
하지만, 가급적으로 코드의 일관성을 위해 최대한 index를 사용하지 않는 것을 추천
기존의 useState를 사용하며, 대부분 setState시에 새로운 상태를 파라미터로 넣어준다.
대신 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데, 이렇게 하면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 값을 넣어주지 않아도 된다.
// 예시) 삭제 함수
const onRemove = useCallback(
id => {
setTodos(todos.filter(todo => todo.id !== id));
},
[todos],
);
// 예시) 함수형 업데이트 후
const onRemove = useCallback(id => {
setTodos(todos => todos.filter(todo => todo.id !== id));
}, []);
input 태그에 onChange 이벤트를 줄 때 타잎이을 할때마다 해당 컴포넌트가 렌더링 되어, 최적화 방법을 찾는데 lodash
라는 최적화 라이브러리가 있다.
물론 라이브러리를 쓰지 않고도 최적화 할 수 있는 방법이 있다
// 예시) 최적화 전(X)
//UserList.jsx
function UserList() {
{...}
return (
<div>
<input
type="text"
value={text}
placeholder="아무 내용이나 입력하세요."
onChange={(event) => setText(event.target.value)}
/>
{...}
</div>
);
}
export default UserList;
// 예시) 최적화 후(O)
//UserList.jsx
function UserList() {
{...}
return (
<div>
<input
ref={searchRef}
type="text"
placeholder="아무 내용이나 입력하세요."
onKeyUp={() => {
let searchQuery = searchRef.current.value.toLowerCase();
setTimeout(() => {
if (searchQuery === searchRef.current.value.toLowerCase()) {
setText(searchQuery);
}
}, 400);
}}
/>
{...}
</div>
);
}
export default UserList;
리액트는 단방향 하향식 데이터 흐름을 가지고 있어, 부모 컴포넌트에서 자식 컴포넌트 방향으로 데이터(props, state)가 흘러간다.
이 데이터들의 변화는 컴포넌트를 리렌더링 시키는데, state는 그것이 선언된 컴포넌트 내에서 사용되고, props는 부모 컴포넌트로부터 받은 데이터이다.
이런 기본구조를 숙지하고 가자!
요즘 CS지식을 다시 공부하면서 놓치고 간 부분들이 아주 많다고 느낀다. 새로 공부하는 게 즐겁기도 하고 React Native로 앱을 공부하고 있는데 솔직히 웹보다 더 재밋는 느낌이다. 끝나면 더 공부하고 싶다
Reference React 렌더링 성능 최적화 방법