프론트엔드를 시작해보겠다고 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라는 언어로 작성되어 빠른 빌드가 가능하게 되었다. 다만 Webpack은 HMR외에도 code splitting과 같은 기능을 종합적으로 제공했기에 복잡한 앱에서 ES Build가 대체할 수는 없었다.
⚡이제는 Vite를 씁시다!
Vite(바이트가 아니다 빗!!! 이다.) 페이지만 들어가도 빠르다라는 키워드를 자주 접할 수 있다. 현대의 브라우저는ESM(ES Modules)가 추가되었고, 위에서 언급한 ES Build를 활용하여 Webpack을 대체한다. 기존의 번들러는 소스 코드가 변경되면 전체적으로 번들링 과정을 다시 거쳐야하다 보니 서비스가 거대해질수록 빌드시간이 늘어나게 된다. Vite는 HMR방식을 지원하는데, 여기서 포인트는 ESM을 활용하여 수정해야 할 부분만 소스 코드만 반영할 수 있게 처리했다.
HMR: 앱을 종료하지 않고 갱신된 파일만 교체하는 방식
dependencies와 source code로 구분하고 개발시 내용이 바뀌지 않는 부분들은 ES Build로 번들링하고 수정하는 부분만 ESM으로 소스 코드를 반영한다. ESM은 요청받은 모듈만 전달하기 때문에 훨씬 빠르게 서버에 반영이 된다.
dependencies: 개발시 내용이 바뀌지 않을 JS코드로 기존에는 매우 비효율적인 시간으로 번들링이 이루어졌지만 Vite는 ESBuild를 통해 10~100배 가까이 빠른 속도를 제공한다.
source code: JSX, CSS와 같이 컴파일링이 필요하고 수정이 잦은 부분들을 말한다. Vite는 브라우저가 요청하는 대로 소스 코드를 변환하고 제공한다.
⚙️Vite으로 React 프로젝트 구성하기
npm create vite@latest 빗을 생성한다.
프로젝트 명을 작성하라고 나온다.(원하는 프로젝트 이름을 입력)
프로젝트 명을 입력하면 원하는 개발 프레임워크를 물어본다. 원하는 개발로 선택한다.
다음은 타입스크립트로 개발할 것인지 SWC를 도입할 것인지 물어본다.
프로젝트 위치로 이동한다. cd 프로젝트명
바로 플젝 시작하면 오류가 난다!!!. npm i를 입력하고 기본 라이브러리 의존 설치를 진행한다.
npm run dev입력하고 프로젝트를 열어본다! 개발 시작!!!
SWC: Rust라는 언어로 제작된 빌드 툴로, JS프로젝트의 컴파일, 번들링을 제공하는 웹 컴파일러 툴이다.
주식이나 뉴스같은 곳에서 한줄로 끊임없이 텍스트가 물처럼 한방향으로 흐르거나 요즘 트렌드의 스타일로 작성된 사이트들을 구경하다 보면 자주 접할 수 있는 스타일의 UI이다. 텍스트뿐만 아니라 관련된 이미지를 통해서도 좀 더 인터렉티브하고 동적으로 움직이다보니 시각적으로 집중이 되는 효과를 확인 할 수 있다. 대표적으로 찾은 기능으로 네이버의 vibe사이트에서 추천 플레이리스트 부분에 아티스트들의 앨범 정보가 끊임없이 흐르는 애니메이션도 볼 수 있는데, 해당 기능을 참고하여 구현하고자 한다.
리액트에서 useMemo를 사용하게 되면 불필요한 렌더링을 막고 앱을 최적화 시킬 수 있다. 여기서 Memo는 Memoization을 뜻하는데, 메모이제이션이란 간단하게 프로그램적으로 반복 행위가 있는 것을 메모리에 저장하여 가져다 사용해서 실행속도를 빠르게 처리하는 기술을 뜻한다. 즉, 한번 동작자체가 부하가 심한 행위가 있는 기능이면서 페이지의 렌더링이 심한 컴포넌트에서 적용하기에 안성맞춤 기술이다. 아직 텍스트만으론 잘 이해가 되질 않는다. 더욱 자세히 알아보겠다.
⚙️useMemo의 구성
function fn(){...}
useMemo(()=> fn(), []);
useMemo는 콜백함수와 의존성 배열 2개의 매개변수를 받는다. 첫번째는 저장(캐싱) 할 함수값을 넣는다. 두번째 의존성 배열부분에는 업데이트 되었을때 저장한 함수가 동작될 요소를 넣어준다. 즉, 배열에 저장된 요소가 업데이트 되면 그때 저장된 함수가 동작된다. 빈 배열을 넣게되면 페이지가 처음 로드되었을때 한번만 캐싱되고 이후부터 사용할 때만 캐싱된 함수를 호출해서 사용한다.
🙃그렇다면 무조건 쓰면 좋을까?
그렇다고 무조건 모든 객체나 원시타입들을 useMemo에 저장해놓고 쓰는게 좋을까? 대답은 No이다. 메모이제이션이기 때문에 결국 useMemo에 등록된 요소들은 결국 어딘가에 저장이 되는데, 개발을 하면서 상황에 맞지 않는 모든 것을 다 저장한다면 오히려 페이지에 부하가 발생하고 느려지는 상황을 초래할 수 도 있다.
일단 부하가 심한 기능이라는 가정을 위해 일부러 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의 개념이 필요한 부분인데 이부분에 대해서 더 자세히 알아보면 좋을 것이다.