반응형

styled-components 내부 속성에 css 변수 적용하기

const ShellButton = styled.button<ButtonStyles>`
  background-color: ${({ theme }) => theme.btnColor};
  color: ${({ theme }) => theme.btnTxtColor};
  border: none;
  border-radius: 10px;
  cursor: pointer;
  &:hover {
    opacity: var(--atomshell-hover-opacity);
  }

  ${({ color }) => color && getButtonColor(color)}
  ${({ size }) => size && getButtonSize(size)}
`;

버튼 컴포넌트를 작성하였고 hover시 0.7정도의 투명도를 지정하였다.
하지만 이부분이 추후 변경될수도 있고, 공통적으로 사용하기 위해 GlobalStyle쪽에서 변수화를 해두었는데, ts에서 오류가 발생하고 있었다.

'--atomshell-hover-opacity' 사용자 지정 프로퍼티를 확인할 수 없습니다

아래처럼 hover부분을 수정하고 해결하였다.

  &:hover {
    opacity: ${'var(--atomshell-hover-opacity)'};
  }
반응형
반응형

⭐️ Vite에서 SVG파일을 컴포넌트로 받아서 사용하기

🚫 오류 사항

vite 프로젝트에서 기존 방식으로 import하여 svg파일을 사용하면 아래와 같은 오류가 발생한다.

'".svg?react"' 모듈에 내보낸 멤버 'ReactComponent'이(가) 없습니다.
대신 '"
.svg?react"에서 ReactComponent 가져오기'를 사용하시겠습니까?ts(2614)

✅ vite-plugin-svgr 적용하기

  • vite-plugin-svgr설치
    vite에서는 바로 svg가 적용이 안되고 vite-plugin-svgr 활용해야 한다고 한다.

    npm install vite-plugin-svgr --save-dev    
  • vite.config.ts

    export default defineConfig({
    plugins: [
      react(),
      svgr({
        svgrOptions: {
          // svgr options
        },
      }),
    ],
    resolve: {
      alias: [
        { find: '@assets', replacement: '/src/assets' },
        { find: '@', replacement: '/src' },
      ],
    },
    });

    plugins 배열에 svgr()을 추가한다(옵션 커스텀이 필요하다면 추가)

  • src/vite-env.d.ts

    /// <reference types="vite/client" />
    /// <reference types="vite-plugin-svgr/client" />  //추가
  • src/SvgTest.tsx

    import SearchIconSVG from '@assets/images/svg/searchIcon.svg?react';
    export const SearchIcon = () => <SearchIconSVG />;

🔹 주의사항

import부분에서 from 끝부분에 "경로?react"를 붙여주고 구조분해할당으로 별칭을 주는게 아
사용할 컴포넌트명을 지정하여 사용하면 된다.

반응형
반응형

.env파일을 통해 환경이나 특정 상황에 따라 변경될 환경 변수값 정의하고 자주 사용하는데, 뒤에 붙는 확장자에 따라서 개발용과 배포용으로 나눠서 사용할수가 있다.

환경변수 사용하기

Vite에서는 약간 사용법이 다른데, cra같은 경우 아래와 같은 명명 규칙을 지키면서 사용한다.
REACT_APP_변수명=값

사용법 예시(환경변수처리)

REACT_APP_BASE_URL=http://localhost:8080.com
REACT_APP_TEST=TEST

사용법 예시(가져오기)

{process.env.REACT_APP_변수명}

환경에 따른 환경변수 적용

.env.local, .env.development, .env.production, .env.test등 개발환경이나거 배포환경에 따라 구분하여 사용할 수 있다.

🎲 .env.local

로컬 개발시 사용한다. test환경 외 모든 환경에서 로딩된다.

🎲 .env.development(Start)

개발 환경에서 사용되며, 아래 명령어로 실행시 동작한다.

$ npm run start
$ yarn start

아래 순서로 동작된다. env.development.local파일이 가장 우선순위가 높다.

.env.development.local > .env.development > .env.local > .env

🎲 .env.production(Build)

서버 배포시 사용되며, 아래 명령어로 실행시 동작한다.

$ npm run build
$ yarn build

아래 순서로 동작된다. env.production.local파일이 가장 우선순위가 높다.

.env.production.local > .env.production > .env.local > .env

🎲 .env.test(Test)

테스트 환경시 사용되며, 아래 명령어로 실행시 동작한다.

$ npm run test
$ yarn test

아래 순서로 동작된다. env.test.local파일이 가장 우선순위가 높다.

.env.test.local > .env.test > .env.local > .env

👀 실행 결과

반응형
반응형

먼저 알아야 할 개념

웹 프론트개발을 시작했다면 먼저 알아야할 지식과 개념들을 먼저 정리한다.

😶‍🌫️CRA(create-react-app)

프론트엔드를 시작해보겠다고 Get Start문서부터 많은 동영상 강의와 책을 보면 항상 cra를 통해 React프로젝트를 작성한다.
cra없이 React프로젝트를 구성하려면 꽤나 많은 의존 모듈 설치와 설정이 필요하기에 너무나도 용이하게 npx create-react-app my-app 한 줄로 설치하면 프로젝트를 작성을 시작할 수 있었다.

🤔모듈 시스템

갑자기 모듈 시스템?... 그래도 알아보자
자바나 C#, C++같은 언어들은 import와 같은 구문을 통해 모듈 시스템을 구축하지만 과거 자바스크립트에는 모듈 시스템이라는 개념이 없었다.
script태그로 필요한 javascript 파일을 작성하거나 연결하고, 그러다보니 컨텍스트가 섞이거나 전역변수가 남발하고 더욱 레거시한 환경으로 넘어가면 let, const도 없으니 var로만 구성된곳에선 변수가 다른 값으로 대입되게 되고... 많은 문제가 있었다.
이런 문제를 해결하고자 처음 나온 개념이 CommonJS의 require, module.export문법이다.
현대의 브라우저는 ESM(ES Modules)덕에 네이티브 모듈 시스템이 생겼다.

  • ESM: import, export와 같은 ES6 표준 모듈 시스템을 말한다.

🔥번들러

이런 문제를 해결하기 위해 Bundler라는 개념이 나오기 시작한다. 기존에는 script형태로 넣다보니 모든 것들을 다 가져오게 되면서 용량문제도 발생하고, 여러 개의 파일이 아닌 한개의 JS파일이 만들어지기 때문에 모듈의 순서와 언제 불러와야 할 지에 대한 순서 문제도 있었는데, 번들러덕에 이런 것들이 해소되기 시작한다.
번들러 개념이 잘 없는 개발자들도 종종 들어본 번들러가 있다. 바로 Webpack으로 cra에도 도입된 번들러이기도 하다.
Webpack덕분에 위에서 발생하던 문제들은 해결되었지만, 속도의 문제가 있었다. JS파일을 하나로 만들어주기 위해 코드 수정이 이루어질때마다 새롭게 빌드를 하게되고 규모가 커질수록 파일이 많아지게되니 시간이 오래걸리면 대기시간이 길어지는 개발자들의 피로도가 엄청나게 늘어났다.

🍬ES Build의 등장

이번엔 ES Build이다.
기존 번들러(Webpack)의 문제를 해결하기 위해 나왔다. 대략 100배 이상 빠르다고 한다.
JS를 기반으로 작성된 번들러와 달리 Go라는 언어로 작성되어 빠른 빌드가 가능하게 되었다.
다만 WebpackHMR외에도 code splitting과 같은 기능을 종합적으로 제공했기에 복잡한 앱에서 ES Build가 대체할 수는 없었다.

⚡이제는 Vite를 씁시다!

Vite(바이트가 아니다 빗!!! 이다.) 페이지만 들어가도 빠르다라는 키워드를 자주 접할 수 있다.
현대의 브라우저는ESM(ES Modules)가 추가되었고, 위에서 언급한 ES Build를 활용하여 Webpack을 대체한다.
기존의 번들러는 소스 코드가 변경되면 전체적으로 번들링 과정을 다시 거쳐야하다 보니 서비스가 거대해질수록 빌드시간이 늘어나게 된다.
ViteHMR방식을 지원하는데, 여기서 포인트는 ESM을 활용하여 수정해야 할 부분만 소스 코드만 반영할 수 있게 처리했다.

  • HMR: 앱을 종료하지 않고 갱신된 파일만 교체하는 방식

dependenciessource code로 구분하고 개발시 내용이 바뀌지 않는 부분들은 ES Build로 번들링하고 수정하는 부분만 ESM으로 소스 코드를 반영한다. ESM은 요청받은 모듈만 전달하기 때문에 훨씬 빠르게 서버에 반영이 된다.

  • dependencies: 개발시 내용이 바뀌지 않을 JS코드로 기존에는 매우 비효율적인 시간으로 번들링이 이루어졌지만 Vite는 ESBuild를 통해 10~100배 가까이 빠른 속도를 제공한다.
  • source code: JSX, CSS와 같이 컴파일링이 필요하고 수정이 잦은 부분들을 말한다. Vite는 브라우저가 요청하는 대로 소스 코드를 변환하고 제공한다.

⚙️Vite으로 React 프로젝트 구성하기

  1. npm create vite@latest 빗을 생성한다.
  2. 프로젝트 명을 작성하라고 나온다.(원하는 프로젝트 이름을 입력)
  3. 프로젝트 명을 입력하면 원하는 개발 프레임워크를 물어본다. 원하는 개발로 선택한다.
  4. 다음은 타입스크립트로 개발할 것인지 SWC를 도입할 것인지 물어본다.
  5. 프로젝트 위치로 이동한다. cd 프로젝트명
  6. 바로 플젝 시작하면 오류가 난다!!!. npm i를 입력하고 기본 라이브러리 의존 설치를 진행한다.
  7. npm run dev입력하고 프로젝트를 열어본다! 개발 시작!!!
  • SWC: Rust라는 언어로 제작된 빌드 툴로, JS프로젝트의 컴파일, 번들링을 제공하는 웹 컴파일러 툴이다.
반응형
반응형

⚡Vite에서 환경 변수 설정하고 호출하기

cra에서 환경변수 사용법과 약간의 차이가 있어서 정리하고자 한다.

⚙️Vite에서 환경 변수 설정하기

먼저 .env파일을 프로젝트 최상단에 생성합니다.

vite에서 호출하는 환경 변수는 VITE_ 라는 접두사를 붙여서 작성합니다.
(문자열이라도 "", ''를 제외한 값만 입력합니다.)
.env

VITE_WHEATHER_KEY=1a2b3c4d5e6f7g8k123456789
HELLO_KEY=HELLO // 동작 안함

😶‍🌫️소스에서 환경변수 호출하기

사용하고자 하는 소스에서 아래의 규칙으로 호출합니다.
import.meta.env.환경변수명

sample.tsx

export default function Title(){
    console.log(import.meta.env.VITE_WHEATER_KEY);
    console.log(import.meta.env.HELLO_KEY);
    return null;
}

🪄출력결과

HELLO_KEY라는 값은 VITE_ 접두사가 없기때문에 출력부분에 undefined처리가 되는 걸 볼 수 있습니다.
반응형
반응형

🎞️ 롤링 슬라이드

주식이나 뉴스같은 곳에서 한줄로 끊임없이 텍스트가 물처럼 한방향으로 흐르거나 요즘 트렌드의 스타일로 작성된 사이트들을 구경하다 보면 자주 접할 수 있는 스타일의 UI이다.
텍스트뿐만 아니라 관련된 이미지를 통해서도 좀 더 인터렉티브하고 동적으로 움직이다보니 시각적으로 집중이 되는 효과를 확인 할 수 있다.
대표적으로 찾은 기능으로 네이버의 vibe사이트에서 추천 플레이리스트 부분에 아티스트들의 앨범 정보가 끊임없이 흐르는 애니메이션도 볼 수 있는데, 해당 기능을 참고하여 구현하고자 한다.


바이브 url: https://vibe.naver.com/about/.

🤔 동작 방식

구현하기 전 구현에 대한 설명이 필요하다.

  1. 먼저 슬라이드 형태로 흘러가야할 요소들을 한줄로 세워준다.
  2. 똑같은 요소들을 하나 더 만들어준다.
  1. css 속성을 통해 한줄로 이어서 붙여준다. (부모속성에 flex와 nowrap처리를 통해 쉽게 구현이 가능하다.)
  1. 한줄이 된 슬라이드를 각기 다른 옵션으로 애니메이트를 처리한다.
  2. 첫번째 원본 영역을 10초간 이동하면 그동안 빈 공백의 10초를 기다려야 하는데, 이때 복사 영역으로 똑같이 붙여서 10초간 이동시키고 무한으로 돌린다.
  3. 슬라이드 영역에 마우스를 올리면 animation-play-state의 값을 paused처리 하는 클래스를 토글 형태로 처리하면 된다.
    .stop{
     animation-play-state: paused;
    }

⚙️ 구현에 필요한 기술

css 레이아웃 개념과 animation의 개념, @keyframe 사용법 익히면 별다른 라이브러리 없이 구현이 가능하다.
hook또한 useState하나만 사용하여 처리 할 예정이다.
아래 구현 예시를 확인해보자.

🪄 구현 시작

HomePage.tsx

import { useState } from "react";
import "./Homepage.scss";

const slides = [
    { color: "red", target: "#" },
    { color: "orange", target: "#" },
    { color: "yellow", target: "#" },
    { color: "green", target: "#" },
    { color: "blue", target: "#" },
    { color: "navy", target: "#" },
    { color: "purple", target: "#" },
];

export default function Homepage() {
    const [animate, setAnimate] = useState(true);
    const onStop = () => setAnimate(false);
    const onRun = () => setAnimate(true);

    return (
        <div className="wrapper">
            <div className="slide_container">
                <ul
                    className="slide_wrapper"
                    onMouseEnter={onStop}
                    onMouseLeave={onRun}
                >
                    <div
                        className={"slide original".concat(
                            animate ? "" : " stop"
                        )}
                    >
                        {slides.map((s, i) => (
                            <li
                                key={i}
                                className={i % 2 === 0 ? "big" : "small"}
                            >
                                <div
                                    className="item"
                                    style={{ background: s.color }}
                                ></div>
                            </li>
                        ))}
                    </div>
                    <div
                        className={"slide clone".concat(animate ? "" : " stop")}
                    >
                        {slides.map((s, i) => (
                            <li
                                key={i}
                                className={i % 2 === 0 ? "big" : "small"}
                            >
                                <div
                                    className="item"
                                    style={{ background: s.color }}
                                ></div>
                            </li>
                        ))}
                    </div>
                </ul>
            </div>
        </div>
    );
}

Homepage.scss

* {
    padding: 0;
    margin: 0;
}
ul,
li {
    list-style: none;
}

.wrapper {
    .slide_container {
        overflow: hidden;

        .slide_wrapper {
            display: flex;
            flex-wrap: nowrap;
        }
        .slide {
            display: flex;
            align-items: center;
            flex-wrap: nowrap;
            position: relative;
            border-top: 1px solid #bbb;
            border-bottom: 1px solid #bbb;
            padding: 40px 0;

            &::before {
                content: "";
                display: block;
                width: 100%;
                height: 1px;
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: #bbb;
                z-index: 1;
            }
            &.original {
                animation: 10s linear infinite normal none running
                    infiniteAnimation1;
            }
            &.clone {
                animation: 10s linear infinite infiniteAnimation2;
            }
            &.stop {
                animation-play-state: paused;
            }

            li {
                margin: 0 80px;
                cursor: pointer;
                z-index: 2;
                transition: 0.3s;
                transform: scale(1);
                &:hover {
                    transform: scale(0.98);
                    &::after {
                        content: "";
                        position: absolute;
                        top: 0;
                        left: 0;
                        bottom: 0;
                        right: 0;
                        width: 100%;
                        height: 100%;
                        background-color: rgba(0, 0, 0, 0.2);
                    }
                }
                &.big {
                    width: 280px;
                    height: 280px;
                }
                &.small {
                    width: 200px;
                    height: 200px;
                }

                .item {
                    width: 100%;
                    height: 100%;
                }
            }
        }
    }
}

@keyframes infiniteAnimation1 {
    0% {
        transform: translateX(0%);
    }
    50% {
        transform: translateX(-100%);
    }
    50.1% {
        transform: translateX(100%);
    }
    100% {
        transform: translateX(0%);
    }
}
@keyframes infiniteAnimation2 {
    0% {
        transform: translateX(0%);
    }
    100% {
        transform: translateX(-200%);
    }
}

✨ 구현 결과물

아주 잘된다.
바이브처럼 뒷부분에 선도 그어봤다.
괜찮은 배경이미지와 색상 대신 이미지를 활용하면 더욱 좋은 결과물을 볼 수 있을 것 같다.

(아래는 위 소스를 기반으로 작성한 리액트 샘플 소스를 받아볼 수 있는 github주소입니다.)

github: [https://github.com/myhappyman/infinite_rolling]

 

반응형
반응형

Redux에 대해 알아보기

그동안 전역 상태관리를 위해 recoil로 충분히 사용이 가능했지만, redux 또한 놓칠 수 없는 개념이기에 학습하고 정리하는 내용을 작성해본다.
먼저 관련된 키워드와 내용에 대해 알아보고, 사용법을 알아보고자 한다.

🔹Action

액션은 Reducer와 소통하는 존재이다.
변화가 일어나면 액션이라는 것이 발생하는데, 구분자 형태로 사용된다.

액션 작성 규칙
-액션은 객체(Object)형태로 작성해야한다.
-객체 내부에는 type이라는 필드를 필수적으로 가지고 있어야 한다.

🔹ActionCreator

액션 객체를 만들어주는 함수이다.
해당 함수는 액션을 만들어주다보니 당연히 Object형태를 반환하고 type 필드를 가진다.
그 외에 동작할 명세를 작성한다.

const changeInput = (text:string) => ({
  type: "CHANGE_INPUT",
  text
});

🔹Reducer

변화를 일으키는 함수다.
현재 상태와 전달받은 액션 객체 파라미터를 받는다.
두 개의 값을 비교하여 새로운 상태를 반환한다.
구분자할 값이 많아지면 비교적 if문보단 switch문이 가독성이 좋아서 switch를 사용한다.

const initialState = {
    counter: 1
}
function reducer(state=initialState, action){
    switch(action.type){
        case "INCREASE":
        return state.counter+1
    }
}

🔹Store

프로젝트에 store는 한 개를 가진다.
(여러개를 가질 수도 있지만 복잡해질 수 있음)
스토어에는 현재 어플리케이션의 상태와 리듀서가 들어간다.
그 외에 중요한 내장 함수들로 구성된다.

🔹Dispatch

리듀서에서 stateaction 두 개의 파라미터를 받는데, 여기서 reducer에게 action을 보내는 행위를 dispatch가 처리한다.
dispatch는 리듀서를 불러와서 current state와 내가 작성한 action을 처리한다.

🔹Subscribe

store에 등록된 값의 변화를 감지하다가 변화가 발생하면 콜백 함수가 동작한다.

🔹Vanilla-Redux

npm install redux
const { createStore } = require("redux");

const divToggle = document.querySelector(".toggle");
const counter = document.querySelector("h1");
const btnIncrease = document.querySelector("#increase");
const btnDecrease = document.querySelector("#decrease");

const TOGGLE_SWITCH = "TOGGLE_SWITCH";
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

/**
 * 액션을 가지고 액션 생성 함수를 작성한다.
 * type은 필수값으로 가지고 있어야하며 원하는 형태로 커스텀 할 수 있다.
 */
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

// 초기값 설정
const initialState = {
    toggle: false,
    counter: 0,
};

/**
 * reducer defined.
 * 변화를 일으키는 함수다. state, action 두 개의 값을 받는다.
 */
function reducer(state = initialState, action) {
    switch (action.type) {
        case TOGGLE_SWITCH:
            return {
                // 불변성 유지를 위해 새로운 객체를 return
                ...state,
                toggle: !state.toggle,
            };
        case INCREASE:
            return {
                ...state,
                counter: state.counter + action.difference,
            };
        case DECREASE:
            return {
                ...state,
                counter: state.counter - 1,
            };
        default:
            return state;
    }
}

// store작성 리듀서 함수를 넣어준다.
const store = createStore(reducer);

/**
 * render함수 작성
 */
const render = () => {
    const state = store.getState();
    if (state.toggle) {
        divToggle.classList.add("active");
    } else {
        divToggle.classList.remove("active");
    }
    counter.innerHTML = state.counter;
};

render();
store.subscribe(render);

// 구독하기
// const listener = () => {
//     console.log("상태가 업데이트됨.");
// };
// const unsubscribe = store.subscribe(listener);
// unsubscribe();

divToggle.onclick = () => {
    store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
    store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
    store.dispatch(decrease());
};
반응형
반응형

💎useMemo 파헤치기

🤔그래서 useMemo가 뭐지?

리액트에서 useMemo를 사용하게 되면 불필요한 렌더링을 막고 앱을 최적화 시킬 수 있다.
여기서 Memo는 Memoization을 뜻하는데, 메모이제이션이란 간단하게 프로그램적으로 반복 행위가 있는 것을 메모리에 저장하여 가져다 사용해서 실행속도를 빠르게 처리하는 기술을 뜻한다.
즉, 한번 동작자체가 부하가 심한 행위가 있는 기능이면서 페이지의 렌더링이 심한 컴포넌트에서 적용하기에 안성맞춤 기술이다.
아직 텍스트만으론 잘 이해가 되질 않는다. 더욱 자세히 알아보겠다.

⚙️useMemo의 구성

function fn(){...}
useMemo(()=> fn(), []);

useMemo는 콜백함수와 의존성 배열 2개의 매개변수를 받는다.
첫번째는 저장(캐싱) 할 함수값을 넣는다.
두번째 의존성 배열부분에는 업데이트 되었을때 저장한 함수가 동작될 요소를 넣어준다.
즉, 배열에 저장된 요소가 업데이트 되면 그때 저장된 함수가 동작된다.
빈 배열을 넣게되면 페이지가 처음 로드되었을때 한번만 캐싱되고 이후부터 사용할 때만 캐싱된 함수를 호출해서 사용한다.

🙃그렇다면 무조건 쓰면 좋을까?

그렇다고 무조건 모든 객체나 원시타입들을 useMemo에 저장해놓고 쓰는게 좋을까?
대답은 No이다.
메모이제이션이기 때문에 결국 useMemo에 등록된 요소들은 결국 어딘가에 저장이 되는데, 개발을 하면서 상황에 맞지 않는 모든 것을 다 저장한다면 오히려 페이지에 부하가 발생하고 느려지는 상황을 초래할 수 도 있다.

😤그럼 언제 써야합니까?!

예시가 제일 빠른 확인이 가능할 것 같다.

import { useState } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {calculator(inputNumber)}
                </div>
            </div>
        </div>
    );
}

일단 부하가 심한 기능이라는 가정을 위해 일부러 slow라는 함수를 정의하여 calculator()내부에서 호출하여 강제로 동기화 기능을 처리하였다.
해당 부분때문에, 약 0.1초 정도의 부하만 줬는데도 부드럽지 않고 끊기는 느낌이 있다.

해당 예제는 input에 입력한 숫자를 추가하면 평균값을 보여주는 간단한 예제인데, 벌써부터 최악인 모습으로 input에 입력할때마다 onChange에서 변경된 state값에 의해 매번 페이지가 재렌더링이 일어나면서 느려터진 slow함수덕에 엄청 버벅거리는 모습을 경험할 수 있다.

실제로도 input에 "1234" 4번만 입력했는데 매번 calculator가 호출되고 있고 있다.

이럴때 바로 useMemo를 사용하면 추가버튼을 누를때만 해당 함수가 호출되어 최적화를 할 수 있다.

최적화하기

import { useState, useMemo } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);

    /*
     * useMemo를 통해 최적화를 한다.
     * 두번째 매개변수 의존 배열에는 해당 값이 바뀔때만 호출될 수 있도록 설정한다
     * 첫번째 매개변수에는 콜백함수를 넣는데 두번째에 넣은 값이 바뀔때마다 해당 콜백이 호출된다.
     */
    const avg = useMemo(() => calculator(inputNumber), [inputNumber]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {avg}
                </div>
            </div>
        </div>
    );
}

이제 초기 렌더링될 때 한번 호출되고 input에 입력해보면 전혀 부하가 발생하지 않는 걸 볼 수 있다.
숫자를 입력하고 추가버튼을 누르면 그때 콜백함수가 호출되면서 calculaotr가 호출된다.

⚠️유의사항

어느정도 useMemo를 언제 사용해야할지와 사용법에 대해 알아보았는데, 조심해야할 부분이 있다.
자바스크립트에는 원시타입과 객체타입의 개념이 있다.
원시타입의 경우 특정 정수나 문자열이 있을 때, 해당 값을 주소값에 넣어두고 같은 값이라면 어떤 변수든 같은 주소값을 바라보기에 아래와 같이 비교 연산이 가능하다.

const test = "hello";
const test2 = "hello";
test === test2 // true 같은 주소값을 바라본다.

하지만 객체타입인 배열이나 객체의 종류의경우 비교연산시 문제가 발생한다.

const obj = {name: "mike"}; // 0x01 에 할당
const obj2 = {name: "mike"}; // 0x02에 할당
obj === obj2 // false 주소값이 서로 다르다.

shallow copy, deep copy의 개념이 필요한 부분인데 이부분에 대해서 더 자세히 알아보면 좋을 것이다.

객체타입의 오류

아래 예제를 보면 위에서 왜 객체 타입에 대한 개념을 언급했는지 알 수 있다.

import { useState, useEffect } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    const obj = { name: regitName };

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

obj라는 객체를 useState에서 받아온 regitName값으로 정의하는 경우 해당 경우에도 매번 재 렌더링이 발생한다.
새로운 객체를 계속해서 새로 넣는다고 판단되기 때문인데, 이런 경우에도 useMemo를 통해 처리가 가능하다.

객체 개선하기

import { useState, useEffect, useMemo } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    // const obj = { name: regitName };
    const obj = useMemo(() => {
        return { name: regitName };
    }, [regitName]);

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

위 처럼 obj객체를 useMemo로 감싸서 처리하면 추가 버튼을 누르는 경우에만 regitName이 동작하면서 useEffect의 콘솔값이 그때 찍히는걸 볼 수 있다.

반응형