TIL 6 -24

hoin_lee·2023년 6월 24일
0

TIL

목록 보기
192/236

Algorithm

KAKAO BLIND RECRUITMENT 1차 캐시

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

React의 렌더링 성능 향상시키기

일단 컴포넌트의 리렌더링 되는 조건

  • 부모에서 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때
  • 자신의 state가 변경 될 때

1. useMemo

React Hook 중 하나로서 React에서 CPU 소모가 심한 함수들을 캐싱하기 위해 사용
만약 텀포넌트 내의 어떤 함수가 값을 리턴하는데 하나의 변화에도 값을 리턴하는데 많은 시간을 소요한다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 된다.
또 함수가 return 되는 값이 자식 컴포넌트에도 사용 된다면, 그 자식 컴포넌트도 함수가 호출 될 때마다 새로운 값을 받아 리렌더링 된다.

만약 컴포넌트 내에 어떤 함수가 값을 리턴하는데 많은 시간을 쓴다면 이 컴포넌트가 리렌더링 될 때마다 함수가 호출 되면서 많은 시간을 소요할 것이고 함수가 반환하는 값을 하위 컴포넌트가 사용한다면 그 하위 컴포넌트는 매 함수 호출마다 새로운 값을 받아 리렌더링 할 것이다.

useMemo(()=> func, [input_dependency])

func은 캐시하고 싶은 함수, input_dependency는 useMemo가 캐시할 func에 대한 입력의 배열로서 해당 값들이 변경되면 func이 호출된다
이것을 적용하면 input_dependency가 있는 데이터가 변할 때에만 평균을 구하는 연산을 수행

useMemo는 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용한다!
즉, 함수 호출 시간도 세이브 간으하고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지가능

2. React.memo 컴포넌트 메모이제이션

React.memo는 Hook이 아니기 때문에 클래스형 컴포넌트에서 사용 가능하다. 함수형 컴포넌트에서는 shouldComponentUpdate를 사용할 수 없는데, 리액트 공식 문서에서는 그 대안으로 React.memo를 제시하고 있고,
React.memo를 통해 컴포넌트의 props가 바뀌지 않았다면, 리렌더링 하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 해줄 수 있다

export default memo(Item);

이런식으로 가능하다
이럴 경우 만약 List라는 컴포넌트를 리렌더링 시키더라도 새로 추가된 Item만 새로 렌더되고 이미 렌더된 Item들은 리렌더링 되지 않는다

3. useCallback

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가 변경되지 않았다고 인지하게 되어 하위 컴포넌트의 리렌더링을 방지할 수 있다.

4. 자식 컴포넌트의 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);

5. 컴포넌트를 매핑할 때는 key값으로 index를 사용하지 않는다.

많이 하는 실수가 컴포넌트를 매핑할 때 key값에 index값을 넣어주는데 이는 최적화에 매우 좋지 않다.
배열 중간에 어떤 요소가 삽입될 때 그 중간 이후에 위치한 요소들은 전부 index가 변경되는데 이로 인해 key값이 변경되어 React는 key가 동일 할 경우, 동일한 DOM Element를 보여주기 때문에 예상치 못한 문제가 발생한다. 또한, 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다.

?? 그러면 index요소는 반드시 사용하면 안될까?
배열의 요소가 필터링, 정렬 삭제, 추가 등의 기능이 들어간다면 문제가 발생할 수 있으나 다음과 같은 경우에서는 index로 사용해도 무방하다
하지만, 가급적으로 코드의 일관성을 위해 최대한 index를 사용하지 않는 것을 추천

  • 배열과 각 요소가 수정, 삭제, 추가 등의 기능이 없는 단순 렌더링만 담당하는 경우
  • id로 쓸만한 unique값이 없을 경우
  • 정렬 혹은 필터 요소가 없어야 함

6. useState의 함수형 업데이트

기존의 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));
}, []);

7. Input에 onChange 최적화

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 렌더링 성능 최적화 방법

profile
https://mo-i-programmers.tistory.com/

0개의 댓글