이번주는 여러모로 만족스러운 한주다. 주도적인 학습과 운동 그리고 프로젝트 모두 재밌고 탄력이 붙는 느낌이 든다. 이번 주는 올림픽 웹 서비스 예제를 활용하여 각자 실습을 해보고 토론하는 방식으로 이루어졌고 총 7명의 스터디원 중 딩코님께서 다양한 방면의 질문 보따리를 풀어주셔서 더할 나위없이 즐겁고 감사하다. 스터디 중 나온 질문들 중 몇가지를 꼽아 블로그에 작성해야겠다. 분명 프론트엔드 취준생분들에게 도움이 될 것이라고 생각한다.
교제에서는 가로 막대가 늘어날때 부드럽지 않다고 하는데 왜 부드러운지 모르겠다. 아하! CPU 6x slowDown으로 설정해보니, jank (끊어져 보이는) 현상이 보인다.
사전 지식: 브라우저 렌더링 과정을 알아야 애니메이션의 원리를 이해하는데 도움이 된다.
핵심은 일반적으로 사용하는 디스플레이는 60Hz이다. 즉 1초에 60장의 화면을 빠르게 보여준다는 의미인데,브라우저도 이에 맞춰져있다고 한다.
브라우저 렌더링 과정을 간략하게 정리하면
서버로부터 html,css 리소스 로드하고 브라우저의 렌더링 엔징이 파싱하여 DOM과 CSSOM을 만든다. 두 개의 객체를 합쳐 렌더트리를 만든다. 만들어진 렌더트리로 각 요소의 레이아웃을 계산하는데 쓰인다.
렌더 트리가 완성되면 화면에 구성해야할 요소들의 위치를 계산한다. 이 과정을 layout이라고 한다.
계산이 끝나면 색을 채워넣는 과정이 진행된다. 이것은 paint.
마지막으로 각 레이어를 합치는 과정이 있다. composite!!
즉 정리해보면
DOM + CSSOM(렌더링 엔진) -> render Tree -> layout -> paint -> composite
더 나아가 중요한 개념이 있다.
애니메이션 최적화 부분을 다루고 있는 만큼 리플로우와 리페인트를 최소화하기 위해 취할 수 있는 방법은 없을까?
리플로우와 리페인트를 해결하기 위해서 GPU를 이용하면 된다고 한다!!
transform과 opacity와 같은 속성을 사용하면 된다. 이런 속성은 브라우저 단에서 작업을 GPU로 위임해서 처리해서 레아아웃과 페인트 단계를 뛰어 넘을 수 있다. 이 개념을 하드웨어 가속이라고 한다.
CPU가 GPU에게 어떤 처리를 위임하는 방법을 말한다.
하드웨어 가속은 프로그램이 지원하는 컴퓨터의 하드웨어를 사용하여 소프트웨어에서 수행할 수 있는 것보다 더 효율적으로 일부 기능을 수행하는 경우를 말합니다. 하드웨어는 CPU 단독으로 실행되는 소프트웨어보다 일부 기능을 더 빠르게 수행하도록 설계되었습니다.
출처: 비디오바이트 공식문서
transform : translate()은 처음부터도 레이어를 분리하지 않고 변화가 일어나는 순간 레이어를 분리한다.
transform : translate3d() 혹은 will-change 속성은 처음부터 분리해서 변화에 더욱 빠르게 대처할 수 있다.
만약 width로 애니메이션을 줄 경우 아래와 같이 계속 리플로우가 발생해서 정크 현상이 생긴다.유저의 네트워크 상황에 따라 끊길 수 있다.
width, height, color 왜냐하면 위에서 봤듯이 브라우저 렌더링 단계에서 layout 및 paint 단계에 반영되는 요소이기에 reflow와 repaint를 일으킨다고 생각하면 이해가 되지 않을까 싶다.
그렇다면 어떻게 width를 수정할까?
예를 들면 아래와 같은 코드를 수정하려면
transform: scaleX($ {({width}) => width / 100});
transform-origin: center left;
여기서 교제에서 알려준 하나의 팁은 scale의 기준점은 항상 center가 기본이다. 그래서 left로 옮겨줘야한다.
//전
width: ${({width}) => width}%;
transition: width 1.5s ease;
//후
width: 100%;
transform: scaleX(${({width}) => width/100});
transform-origin: center left;
transition: transform 1.5s ease;
네트워크 패널으로 분석해본 결과 width로 애니메이션을 부여했을때는 reflow가 일어나는 것을 확인했다. transform으로 애니메이션을 수정해보니 아래와 같이 놀라운 결과를 볼 수 있었다. 알리고 올리고 프로젝트에 적용해봐야지!
리액트 코드에서 suspense 와 lazy 함수를 활용하여 동적으로 import하여 컴포넌트 로딩을 지연하였다. 그래서 생긴 문제는 컴포넌트가 마운트될때 js를 로드하기에 아래와 같이 약 1초에서 느리면 2초까지 아무일도 일어나지 않다가 모달이 뜨는 문제가 발생했다.
그럼 사전에 로딩하는 기점은 어떻게 선정할 것인지가 중요한 문제 그래서 책에서 추천하는 방식은 두가지가 있다. 첫번째는 유저가 해당 DOM에 mouse가 enter되었을때 파일을 로드하도록 하는것. 두번째는 컴포넌트가 마운트가 끝나면 그때 다른 파일을 로드하도록 하는 것.
const handleMouseEnter = () => {
const component = import("./components/ImageModal");
};
<button
onMouseEnter={handleMouseEnter}
>
useEffect(() => {
const component = import("./components/ImageModal");
}, []);
이미지는 화면에 그려지는 시점 즉 html 혹은 css에서 이미지를 사용하는 시점에 로드된다. Js를 활용하여 이미지를 직접 로드할 수 있는 방법이 있다. 바로 Image 객체를 사용하는 것.
chart.js를 사용할때 사용해본 경험이 있는데 프로젝트에 적용해보자
알리고 올리고라는 취준생들을 위한 목표 달성 웹서비스 내에 target, 즉 목표를 만드는 퍼널이 있다. 그 안에서 단계를 나타내기 위해 Progress Bar를 만들었다. 너무나 감사하게 과거의 나는 width를 활용해서 Bar를 만들었고 바로 적용해보려고 한다. 현재 보여지는 UI는 아래와 같다.
import { useState } from "react";
//... 수많은 import
const TargetCreate = () => {
const targetService = useTarget();
const navigate = useNavigate();
const [message, setMessage] = useState("");
const [step, setStep] = useState<TargetStepType>("goal");
const methods = useForm({
defaultValues: {
subGoal: [{}, {}, {}],
routine: [{}],
},
resolver: yupResolver(targetSchema),
});
const onSubmitHandler = (data: TargetInfoType) => {
targetService
?.postTarget(data)
.then((res) => {
navigate("/target");
})
.catch((error) => setMessage(error.APIMessage));
};
return (
<div className=" flex flex-col items-center h-screen px-6 pb-10 relative">
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(onSubmitHandler)}
className="w-full relative"
>
<CreateBar step={step} /> // 이부분을 살펴볼 예정이다.
<Step check={step === "goal"}>
<Goal setStep={setStep} />
</Step>
<Step check={step === "subGoal"}>
<SubGoalRoutine setStep={setStep} />
</Step>
<Step check={step === "duration"}>
<Duration setStep={setStep} />
</Step>
<Step check={step === "lastStep"}>
<LastStep setStep={setStep} />
</Step>
</form>
</FormProvider>
</div>
);
};
export default TargetCreate;
import { LoudOli } from "../../../utils/constant/image";
type Props = {
step: keyof typeof getPercentFormStep;
};
const getPercentFormStep = {
goal: `w-[25%]`,
subGoal: `w-[50%]`,
duration: `w-[75%]`,
lastStep: `w-[100%]`,
};
const CreateBar = ({ step }: Props) => {
return (
<div className="py-4 px-4 sticky top-0 bg-white overflow-x-hidden overflow-y-hidden">
<div className="h-2 w-full bg-[#e0e0de] rounded-md relative">
<div
className={`h-full bg-main rounded-md text-xs ${getPercentFormStep[step]} origin-left transition-all duration-300`}
></div>
</div>
</div>
);
};
export default CreateBar;
import { LoudOli } from "../../../utils/constant/image";
type Props = {
step: keyof typeof getPercentFormStep;
};
const getPercentFormStep = {
goal: `scale-x-25`,
subGoal: `scale-x-50`,
duration: `scale-x-75`,
lastStep: `scale-x-100`,
done: `scale-x-100`,
};
const getLocationFromStep = {
goal: `translate-x-1/4`,
subGoal: `translate-x-1/2`,
duration: `translate-x-3/4`,
lastStep: `translate-x-full`,
done: `scale-x-100`,
};
const CreateBar = ({ step }: Props) => {
return (
<div className="py-4 px-4 sticky top-0 bg-white overflow-x-hidden overflow-y-hidden">
<div className="h-2 w-full bg-[#e0e0de] rounded-md relative">
<div
className={`h-full bg-main rounded-md text-xs ${getPercentFormStep[step]} origin-left transition-transform duration-300`}
></div>
<div className={`${getLocationFromStep[step]}`}>
<img
src={LoudOli}
alt="oli"
className={`absolute h-8 -left-5 transform -translate-y-1/2 z-10 transition-transform duration-300`}
/>
</div>
</div>
</div>
);
};
export default CreateBar;
우선 Performance 패널에서 network와 CPU를 각각 낮춰주고 분석을 시작했다.
놀랍게도, 확연한 차이가 눈에 보인다. 우선 width로 애니메이션을 만들었을때는 위치가 변경되면 layout과정이 계속 반복된다. 아주 작은 프로젝트인 알리고 올리고에서도 이렇게 눈에 보이는데 큰 프로젝트였다면 유의미한 성능 차이를 보일 것이다.
아래는 transition을 활용했고 GPU에게 위임했기에 널널해보인다.
최대한 동일한 조건으로 테스트하기 위해 버튼이 클릭되고 애니메이션이 끝나는 시점의 Summary를 비교해보려고 했지만 한시간 내내 분석해봐도 시원한 결과는 나오지 않았다. transition이 확실히 width보다는 painting은 덜 되는 것으로 보이나 다른 부분에서 시간이 더 소요돼, 결과는 비슷한데 조금 더 알아봐야겠다.
생각보다 유의미한 결과를 얻지 못했다... 죄송합니다 하지만 분명 transition으로 만든 애니메이션이 repaint의 빈도가 현저히 낮았다. 분석을 하기 위해서 동일한 환경에서 분석을 해야하는데 여러가지의 변수가 있어 분석하는데 어려움이 있었다. transition일때 왜 JS 파일이 더 큰지?등 아마 분명 다른 이유가 있겠지만! 오늘은 프로젝트에 적용해보면서 2주차 스터디를 통해 알게 된 CSS 애니메이션 최적화를 적용해보았다.
개인 프로젝트에 적용해보면서 머리로 알던 것들을 깨닫는 재미가 솔솔하다. 확실히 여러 명이 같이 토론의 형태로 여러 가지 주제에 대해 깊이 이야기해볼 수 있어서 역시나 유익하고 재밌다. 이번주는 시간이 너무 빨리 지나가버려 정신차려보니 1시간 30분이 지나 아쉬웠다.
정말 좋은 글이었어요, 감사합니다.