코딩 애플 React Part 2 - (4)

신승준·2022년 8월 4일
0

localStorage로 만드는 최근 본 상품 기능 1

  • 새로고침 시 state 등이 코드에 설정된 값으로 초기화된다.

    • 새로고침해도 우리가 저장한 그대로 남아두게 하려면 서버에 데이터를 저장하고 불러오면 된다.
    • 하지만 이번엔 반영구적으로 보관할 수 있는 Local Storage를 사용해보겠다.
  • Local Storage

    • 문자 데이터만 저장이 가능하며 5MB까지 저장할 수 있다.
    • 사이트 재접속해도 영구적으로 남아 있다. 브라우저 청소 시 삭제된다.
  • Session Storage

    • Local Storage와 같이 브라우저에 데이터 저장이 가능하다.
    • 하지만 브라우저를 껐다 키면 사라진다.
    • 휘발성 데이터를 저장하기에 적합하다.
  • Local Storage에 데이터 저장 및 가져오기, 삭제

    • '20'이 아니라 20으로 숫자형을 적어도 문자형으로 저장이 된다.
    • update는 따로 없다. get으로 값을 확인한 뒤, remove 후 다시 set을 해주는 방식 등으로 업데이트해줘야 한다. 값을 미리 알고 있다면 같은 key에 다른 value로 setItem하면 중복되어 쌓이지 않고 덮어씌워지며 update된다.
localStorage.setItem('age', '20');
localStorage.getItem('age');
localStorage.removeItem('age');
  • Session Storage도 위와 같이 앞의 이름만 바꾸면 된다.

  • object/array 자료는 저장이 불가능하고 문자만 가능하다.

    • 하지만 트릭을 이용한다.
    • JSON으로 바꿔준다.
  • 다음과 같이 하면, 저장이 제대로 되지 않는다.

function App() {
	// Local Storage에 저장해보기
	let obj = {name: 'kim'};
	localStorage.setItem('data', obj);

  • JSON.stringify()를 이용하면 문자형으로 저장된다.
    • 문자형이기 때문에, object나 array에서 원소를 뽑듯이 뽑을 수 없다.
function App() {
	// Local Storage에 저장해보기
	let obj = {name: 'kim'};
	localStorage.setItem('data', JSON.stringify(obj));

  • JSON.parse()를 통해 JSON 자료를 object/array로 변환할 수 있다.
function App() {
	// Local Storage에 저장해보기
	let obj = {name: 'kim'};
	localStorage.setItem('data', JSON.stringify(obj));
	let temp = localStorage.getItem('data');
	console.log(JSON.parse(temp));
	console.log(JSON.parse(temp).name);




localStorage로 만드는 최근 본 상품 기능 2

  • 과제

App.js

useEffect(() => {
	let watched = Array();
	localStorage.setItem('watched', JSON.stringify(watched));
}, [])

Detail.js

useEffect(() => {
    let watched = JSON.parse(localStorage.getItem('watched'));
    if (Object.values(watched).includes(detail.id) === false) {
        watched.push(detail.id);
        localStorage.setItem('watched', JSON.stringify(watched));
    }
}, [])

혹은 다음과 같이 set 자료형으로 만들어서 할 수도 있다.

Detail.js

useEffect(() => {
        let watched = JSON.parse(localStorage.getItem('watched'));
        watched.push(detail.id);
        watched = new Set(watched); // 중복 제거
        watched = Array.from(watched);
        localStorage.setItem('watched', JSON.stringify(watched));
    })

  • 새로고침 시 초기값으로 돌아가지 않게 하기

App.js

useEffect(() => {
  let watched = Array();

  if (localStorage.getItem('watched') === null) {
    localStorage.setItem('watched', JSON.stringify(watched));
  }
}, [])
  • redux-persist 라이브러리를 쓰면 store 안의 state를 Local Storage에 저장할 수 있다.



실시간 데이터가 중요하면 react-query

  • 다음과 같은 경우에 React Query를 유용하게 사용할 수 있다.

    • ajax 성공 시 혹은 실패 시 각각 다른 html을 보여주고 싶다면?
    • 몇 초마다 자동으로 ajax를 요청 하고 싶다면?
    • ajax 실패 시 자동으로 몇 초 후에 요청을 재시도 할 것인지?
    • prefetch, 다음 페이지를 미리 가져오는 것
  • 실시간 SNS(코인 거래소 등) 사이트에서 잘 사용된다.

  • 셋팅

    • 설치 npm install react-query
    • queryClient 생성 후, App 컴포넌트를 QueryClientProvier client={queryClient}로 감싼다.

index.js

...
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <QueryClientProvider client={queryClient}>
        <React.StrictMode>
            <Provider store={store}>
                <BrowserRouter>
                    <App />
                </BrowserRouter>
            </Provider>
        </React.StrictMode>
    </QueryClientProvider>
  • 서버에서 유저이름을 가져와 보여주기

App.js

...

import { useQuery } from 'react-query';

...

useQuery를 쓸 때 장점

  1. 해당 요청이 성공했는지, 실패했는지, 로딩 중인지 쉽게 파악이 가능해진다.
os 앞의 return{} 생략 가능
let result = useQuery('name', () => {
	return axios.get('https://codingapple1.github.io/userdata.json')
	.then((response) => {
		return response.data;
	})
})

result.data			// axios GET 요청 후 성공 시 데이터를 가져온다.
result.isLoading	// Load 중일 때(요청 중일 때) true가 된다.
result.error		// axios GET 요청 후 실패 시 true가 된다.
  • 로딩 중일 때 '로딩 중입니다'를 보여주고 싶다면? + 성공 시, 실패 시 띄울 것들
    - 이 때 쓰인 && 연산자는, 왼쪽 조건문이 true이면 오른쪽 것을 남겨달라는 뜻이다.
<Nav className="ms-auto" style={{ color: 'white', fontWeight: 'bold'}}>
	{result.data && result.data.name}
	{result.isLoading && '로딩 중'}
	{result.error && '에러'}
</Nav>
  1. 틈만 나면 자동으로 ajax를 재요청해준다.(틈만 나면 자동으로 refetch 해준다)
  • 다른 창으로 옮겼다가 react로 띄운 웹을 보면, 콘솔 창에 '요청됨'이 찍히는 것을 볼 수 있다.
    • 즉 다시 ajax 요청을 했다는 뜻이다.
      • 근데 리렌더링 되면서 다시 ajax 요청을 하는 것 같다.
    • 다음과 같이 staleTime을 사용하면, 해당 시간 동안 재요청(refetch)을 하지 않는다.
let result = useQuery('name', () => {
	return axios.get('https://codingapple1.github.io/userdata.json')
	.then((response) => {
		console.log('요청됨');
		return response.data;
	})
}, {staleTime: 2000})
  1. 실패 시 retry를 알아서 해준다.
  • 서버가 죽거나, 이상한 경로로 요청했거나 등등 이럴 경우 자동으로 retry를 해준다.
  1. state 공유를 하지 않아도 된다.
  • 만약 지금 뜨는 Mark를 자식 컴포넌트에서도 쓰고 싶다면, state로 저장하여 자식 컴포넌트로 넘겨야, Mark가 setState로 인해 바뀌었을 때 자식 컴포넌트의 글자도 바뀔 것이다.
    • 즉 axios로 받은 Mark를 자식 컴포넌트에서도 쓰려면 state로 저장해서 넘겨야 한다.
    • 하지만 react-query를 사용하면 그러지 않아도 된다.
    • 부모에서도 useQuery로 axios를 통해 Mark를 가져오고, 자식에서도 useQuery로 axios를 통해 Mark를 가져오면 된다.
    • 그냥 axios로 하면 2번 요청하는 셈으로 비효율적이지만, useQuery + axios로 할 경우 react-query는 이 2번을 알아서 1번만 실행한다.
  1. ajax 결과 캐싱
  • ajax 성공 결과를 5분간 들고 있는다.

    • 다음과 같을 경우, 12시 13분에 자식 컴포넌트에서 ajax 요청을 할 때, 요청을 성공하기도 전에 곧바로 부모 컴포넌트에서 했던(12시 10분에 성공해서 이미 해당 데이터를 5분간 가지고 있다) 요청을 바탕으로 가진 데이터를 곧바로 보여준다.
    • 즉 자식 컴포넌트에서 13분에 실행되어 14분에 끝나더라도, 자식 컴포넌트는 13분에 곧바로 보여줄 수 있게 되는 셈이다.
    • 이렇게 일단 보여주고 그 다음에 GET 요청을 하게 된다.
  • 사실 redux-toolkit을 설치하면 RTK Query가 자동적으로 설치된다. React Query랑 유사하나 문법적으로 더러운 부분이 있다.




성능개선 1: 개발자 도구 & lazy import

  • 브라우저 개발자 도구의 Components에서 컴포넌트들을 구조화해서 보여준다.

    • 해당 컴포넌트에 사용된 props, 훅들(state 등)을 확인할 수 있고 수정해볼 수 있다.
      • 내가 코드로 작성한 props나 훅들이 어떻게 사용되고 있는지 확인할 수 있다.
  • Profiler에서는 성능(렌더링하는 속도)을 측정해볼 수 있다.

  • Redux DevTools

    • Redux로 설정한 state들이 어떻게 변하고 있는지 알 수 있다.

lazy import

  • single page application으로, build 후 배포하게 되면 html, css, javascript 각각 하나의 파일로 뭉쳐진다.

    • 따라서 배포 후 실제 사용자가 해당 사이트에 방문하게 되면 사이즈가 매우 큰 javascript 파일을 다운 받게 된다.
    • 따라서 기본적으로 React로 만든 사이트는 로딩 속도가 느리다.
  • 이렇게 큰 javascript 파일을 잘게 분리해서 다운 받게 해주려면?

    • 각 컴포넌트가 필요할 때, 즉 그 컴포넌트가 보이는 곳으로 접근 혹은 접속하게 되면 lazy import 되도록 한다.
...

import { lazy, Suspense, createContext, useEffect, useState } from "react";

...

// import Detail from "./routes/Detail.js";
// import Cart from "./routes/Cart.js"

// lazy loading
const Detail = lazy(() => import('./routes/Detail'));
const Cart = lazy(() => import('./routes/Cart'));

위와 같이 lazy import가 필요한 곳을 변경한다.

그리고 다음과 같이 필요한 곳에 Suspense로 감싼다.

<Suspense fallback={<Loading></Loading>}>
  <Detail shoes={shoes} />
</Suspense>

로딩 중일 경우 fallback의 컴포넌트들이 보이게 된다.
Routes 안의 컴포넌트 전체를 lazy import하고 Routes를 모두 Suspense로 감싸도 된다. 그렇게 되면 모든 컴포넌트들이 lazy import될 것이다. 이러면 처음 페이지 방문 시 속도가 빨라질 것 같다.




성능개선 2: 재렌더링 막는 memo, useMemo

  • 내가 원하는 자식의 재렌더링을 막아보자.
    • 버튼을 누르면 Child라는 자식 컴포넌트도 재렌더링된다는 것을 알 수 있다.
function Child() {
    console.log('재렌더링됨');
    
    return <div>Child</div>
}

function Cart() {
    let user = useSelector((state) => state.user);
    let dispatch = useDispatch();
    let [count, setCount] = useState(0);

    return (
        <div>
            <Child></Child>
            <button onClick={() => { setCount(count + 1) }}>+</button>

memo

  • 이러한 재렌더링은 성능저하를 일으킬 수 있다. Child가 랜더링 되는 데에 시간이 오래 걸리는 저성능 컴포넌트라면, 부모가 렌더링 될 때 자식의 렌더링을 막아야 한다.(자식이 굳이 렌더링 되지 않아도 된다면)
  • Child가 꼭 필요할 때 렌더링 되도록 하기 위해 memo를 사용해야한다.
    • 변수를 생성하고 memo를 사용하여 컴포넌트로 만든다.
    • 이렇게 하면 + button을 클릭해도 재렌더링됨이라는 로그가 찍히지 않음을 확인할 수 있다. 즉 재렌더링 되지 않는 것이다.
...

import { memo, useState } from 'react';

let Child = memo( function() {
    console.log('재렌더링됨');
    
    return (
        <div>Child</div>
    )
})

function Cart() {
    let user = useSelector((state) => state.user);
    let dispatch = useDispatch();
    let [count, setCount] = useState(0);

    return (
        <div>
            <Child></Child>
            <button onClick={() => { setCount(count + 1) }}>+</button>

...
  • props가 변할 때에만 재렌더링된다. memo를 통해 만든 컴포넌트에, 이런 props가 변했을 때 해당 내용을 반영할 수 있도록 렌더링 되었으면 좋겠다 싶은 props를 전달해주면 된다.
    • 다음과 같이 할 경우 count가 변할 때 Child 또한 재렌더링 된다.
<Child count={count}></Child>
<button onClick={() => { setCount(count + 1) }}>+</button>
  • memo를 사용하면, 부모가 재렌더링될 때 memo 컴포넌트에 사용되는 props가 바뀌는지 확인하는 비교 작업이 추가적으로 들어가게 된다.
    • 만약 props가 많고 길고 복잡하면 이 비교 작업이 오래 걸리게 된다.
    • 따라서 무작위로 사용하면 좋지 않다. 비교 작업이 간결하게 끝날 때, 혹은 memo를 사용하는 컴포넌트가 정말 무거운 컴포넌트일 때 사용하면 좋다.

useMemo

  • useMemo는 useEffect와 유사하다.
    • func라는 함수는 10억번 도는 for문을 실행하여 result를 반환한다. 따라서 상당히 오래 걸리는 작업이다.
    • 이러한 작업이 렌더링 될 때마다 실행된다고 하면 상당히 오래 걸리는 작업이 반복해서 이뤄지게 된다.
    • 이것을 막기 위해 useMemo를 사용한다.
      • dependency에 그냥 []을 쓰면 mount될 때 1번 실행되고 만다.
      • 아래와 같이 user와 같은 dependency를 작성하면, 바뀐 dependency를 반영한 상당히 오래 걸리는 작업을 수행하기 위해 그 오래 걸리는 작업이 실행되게 된다.
    • 실제로 useMemo를 해보면 func 함수는 1번 실행되어 result에 10억이 담겨진 채로 재렌더링해도(count 증가 버튼을 누름) 10억이 한번에 담겨 있고 func 함수를 재실행하지 않아 빠르게 console.log로 찍히는 것을 볼 수 있다.
    • 반면 useMemo를 사용하지 않은 result2는 재렌더링될 때마다 func 함수를 수행하여, 시간이 오래 걸린 후 console.log가 찍히는 것을 확인할 수 있다.
function func() {
    let result = 0;
    
    for (let i = 0; i < 1000000000; i++) {
        result += 1;
    }
    
    return result
}

function Cart() {
    let user = useSelector((state) => state.user);
    let dispatch = useDispatch();
    let [count, setCount] = useState(0);
    
    let result = useMemo(() => {
        return func();
    }, [user]);
    
    let result2 = func();
    
    // console.log(result);
    console.log(result2);
  
    return (
        <div>
            <Child count={count}></Child>
            <button onClick={() => { setCount(count + 1) }}>+</button>
  • 이와 같이, useMemo와 useEffect는 아주 유사하다.
    • 단지 차이는, 실행 시점이다.
    • useEffect는 return문의 JSX가 렌더링(실행)되고 나서 실행되지만,
    • useMemo는, 렌더링될 때 useMemo 안의 함수가 같이 실행된다.



성능개선 3: useTransition, useDeferredValue

automatic batch

  • state 변경 함수가 여러 개 이어져 있으면 각 함수들마다 재렌더링이 일어나는 것이 아니라, 마지막에 1번 재렌더링 된다.
  • ajax 요청, setTimeout 내부에서 작성된 state 변경 함수 실행은 react 17까지는 automatic batch가 일어나지 않았다.

  • 하지만 react 18부터는 automatic batch을 ajax 요청 및 setTimeout 내부에서도 지원하게 되었다.

useTransition

  • 동작이 느린 컴포넌트를 혁신적으로 빠르게 동작시킬 수 있다.
profile
메타몽 닮음 :) email: alohajune22@gmail.com

0개의 댓글