항해 플러스 프론트엔드 코스란?
1~4년 차 주니어 프론트엔드 개발자들이 모여 매주 토요일마다 오프라인 모임, 코드리뷰를 진행하고
평일에 한 번씩 멘토링을 진행하면서 주니어 개발자들이 성장할 수 있는 환경을 제공해 주는 코스입니다.
이번 글에서는 항해 플러스 프론트엔드 코스에서 3주 동안 배운 리액트 기초 커리큘럼에 대해서 작성해 보려 합니다.
해당 챕터에서는 React의 메모이제이션 훅과 언제 리렌더링이 되는지 다룹니다.
사실 실제 현업에서 프론트엔드 개발을 하게 되면 동작 방식을 이해해야 할 정도로 깊은 요구사항을 구현하게 되는 경우가 없습니다. 하지만 동작 방식을 이해하게 되면, 상태 관리와 컴포넌트 생명주기를 효율적으로 사용하고 코드의 가독성과 재사용성을 높일 수 있습니다.
더 나은 성능과 유지보수성을 가진 애플리케이션을 개발할 수 있으며 팀 협업과 개발 효율성을 높일 수 있습니다.
이러한 목적에서 우리는 React의 동작 원리를 이해하고 언제 렌더링이 되는지 알아야 합니다.
프론트엔드 개발자는 UI에 대한 발생하는 문제를 해결하며 최대한 효율적으로 돈을 덜써야 합니다.
React는 DOM을 관리하는 비용이 커서 느리지만, 그래도 사용하는 이유는 생산성이 빨라 돈을 덜 쓰게 됩니다.
1주 차에서는 왜 우리가 리액트를 사용하고, 어떻게 동작하는지 알 수 있게 됩니다.
항해 플러스 홈페이지에서는 수강생 작품으로 1기분들의 “팀 프로젝트”가 있지만, 2기는 개인 과제로 진행한다는 점 참고 부탁드립니다!
1주 차 과제로는 리액트의 동작 원리에 관련된 것보다는 React Hook 중에서 메모이제이션 관련된 것들을 언제 사용하고, 어떻게 최적화하는지 이해하며 구현하는 과제입니다.
과제를 실제 리액트 코드와 테스트 코드가 있어서 테스트 코드를 보며 어떻게 expected를 도출해야 할지 확인하며 리액트 코드를 수정하여 테스트를 통과하면 과제도 PASS 하게 됩니다.
과제 관련된 자세한 사항은 제 github repo를 참고하시면 좋을 것 같아요!
1주 차 과제를 하면서 저는 useCallback과 useMemo, React.memo를 언제 사용해야 하고, 어떻게 리렌더를 방지할 수 있는지 알 수 있게 되었습니다.
우리가 흔히 하는 질문인 "언제 메모이제이션을 사용하는지 모르겠어."의 답을 알 수 있게 될 것입니다.
유저의 입장에서 메모이제이션을 위한 데이터 비교 연산과 리렌더 연산중 어떤 게 더 비용이 많이 들지 확인해 보는 것이 중요합니다. 섣부른 최적화는 독이 되지만, 의심 가는 곳이나 리렌더가 많이 발생하는 곳에는 메모이제이션을 적용하여 비교해 보는 것을 배울 수 있게 됩니다.
해당 챕터에서는 불변성과 상태 관리에 대해서 깊게 배우게 됩니다.
React의 기초로 불변성에 다루는 이유는 상태 관리에서 불변성을 중요하게 생각하기 때문인데요. 불변성을 유지하면 변경사항을 추적하고 최적화하기 쉬워진다고 합니다.
이러한 이유에서 불변성을 이해하고, React에서 값을 어떻게 비교하여 언제 리렌더링이 발생하는지 알 수 있어야 합니다.
1주 차 과제에서는 실제 리액트 코드에서 useCallback, useMemo를 적용시키는 정도의 난이도였다면
2주 차 과제는 실제로 비교 연산과 메소드(forEach, filter, some, every …)를 직접 구현하는 과제여서 제일 어려웠던 과제였다고 생각합니다.
얕은 비교, 깊은 비교를 구현해보면서 기존 JS의 비교 연산을 이해할 수 있게 되고, 리액트에서는 값이 언제 달라졌다고 인식하는지 고민해볼 수 있게 됩니다.
“obj.a, obj.b는 값이 있는데, spread를 하게 되면 왜 빈 객체이지?” 라는 것도 고민할 수 있고
const obj = createUnenumerableObject({ a: 1, b: 2 })
expect(obj.a).toEqual(1);
expect(obj.b).toEqual(2);
expect({ ...obj }).toEqual({});
“number 처럼 동작하지만 type은 왜 object이지?” 라는 것을 고민해볼 수 있습니다.
const num1 = createNumber1(1);
const num2 = createNumber1(2);
expect(num1 + num2).toBe(3);
expect(num1 === 1).toBe(false);
expect(num1 == 1).toBe(true);
expect(typeof num1 === 'number').toBe(false);
expect(typeof num1 === 'object').toBe(true);
3주 차 챕터에서는 리액트의 재조정자 알고리즘과 가상 돔에 대해서 다룹니다.
“React를 만들어보는 게 어떤 도움이 될까?” 의문이 들수도 있습니다.
이러한 질문에 대해 코치님은 이렇게 답하셨습니다.
React를 사용하면서 발생하는 다양한 문제들이 존재하고, 이는 React의 동작 방식을 깊게 이해하면 이해할수록 문제를 잘 해결하거나 문제를 발생하지 않도록 하는 데 도움이 됩니다.
무엇보다 더 좋은 회사로 이직하거나 더 어려운 난이도의 일을 맡아서 진행할 때 React의 동작 방식을 깊게 이해하는 것이 큰 도움이 될 확률이 높습니다. 사실 우리가 하는 일의 대부분은 어떤 개발자든 할 수 있는 일들입니다. 80~90% 정도의 일이 그렇습니다.
하지만 나머지의 10% 정도의 일을 처리하는 것은 무척 어려워요.
그걸 잘할 수 있는 사람의 경쟁력과 가치는 무척 높습니다. 대체할 수 없는 인력이 되어가는 것이죠.
꼭 리액트가 아니더라도, 이처럼 프레임워크가 어떤 식으로 동작하는지 잘 이해할 수 있다면 우리가 일하는 환경이 변하더라도 적응하기가 조금 더 수월해질 것입니다. 다양한 문제 상황에 대해서도 어떤 식으로 문제에 접근하고 해결해야 좋을지 고민하는 시간이 되었으면 합니다.
React를 직접 만들어보면 React의 동작 방식을 깊게 이해할 수 있게 되어 더 어려운 문제를 해결하거나 애초에 문제를 발생시키지 않도록 도움을 줍니다.
3주 차 과제에서는 render를 어떻게 하는지 구현하고, document API(appendChild, replaceChild, removeChild)를 활용하여 재조정 알고리즘을 구현해보게 됩니다.
개념으로만 배워 추상적으로 그리게 되었던 가상돔/재조정 알고리즘을 직접 구현해 보니 동작 원리에 대해서 깊게 고민해 볼 수 있게 됩니다. node를 render 함수에 넘겨 HTML Tree 구조로 어떻게 렌더할 것인지 직접 구현합니다.
const $root = document.createElement("div");
react.render($root, () =>
jsx(
"div",
{ id: "test-id", class: "test-class" },
jsx("p", null, "첫 번째 문단"),
jsx("p", null, "두 번째 문단")
)
);
expect($root.innerHTML).toBe(
`<div id="test-id" class="test-class"><p>첫 번째 문단</p><p>두 번째 문단</p></div>`
);
또한 useMemo를 직접 구현하여 값을 캐싱하고, 의존성이 바뀔 때 바뀐 값을 반환하며 클로저에 대한 개념도 활용해 볼 수 있게 됩니다.
const memo6 = getMemo([1, 2, 3]);
const memo7 = getMemo([1, 2, 3]);
// 의존성이 동일하면 동일한 값을 return
expect(memo6).toBe(memo7);
// 여러개 의존성 중 한개라도 변경되면 새로운 값을 return
const memo8 = getMemo([1, 2, 100]);
expect(memo8).not.toBe(memo6);
“이렇게 동작 원리를 배워서 실제 현업에서 적용해 볼 수 있을까?” 의문이 들 수도 있을텐데요.
저는 실제로 React의 리렌더링 원리를 이용하여 렌더 시간을 95%를 개선해본 경험이 있습니다!
실제로 코드를 보며 이러한 부분은 렌더링이 많이 발생하겠다는 것을 알 수 있게 되어 문제를 발견할 수 있게 되었습니다.
데이터 요청이 언제 마지막인지를 나타내는 “마지막 업데이트: N초전” UI를 구현하는 요구사항이었습니다.
쿼리 요청이 언제 완료되었는지 알아야 하고, 그전까지는 1초마다 타이머를 업데이트 해주어야 합니다.
데이터를 요청하는 쿼리 로직에 타이머 로직도 포함되어 setInerval로 1초마다 호출될 때마다 쿼리도 다시 호출되어 쿼리를 불러오는 모든 컴포넌트에서 리렌더가 발생하였습니다.
setState를 실행하여 상태 값이 바뀌게 되면 해당 상태를 사용하는 모든 곳의 컴포넌트는 리렌더가 됩니다.
1초마다 타이머의 상태가 바뀌게 되니 전체 컴포넌트가 리렌더 되는 것은 당연합니다.
당연한 것을 어떻게 최소화할 수 있을지 고민하게 되었습니다.
제가 생각한 방법은 타이머를 컴포넌트로 분리하여 1초마다 리렌더가 되는 영역을 줄이는 것이었습니다.
setInterval로 타이머를 업데이트하는 로직을 타이머 컴포넌트에만 선언하고, 쿼리에서는 요청이 완료되었을 때 타이머 트리거 상태를 업데이트시켜, 그때 다시 타이머는 0으로 초기화 되도록 해결하였습니다.
타이머 로직을 분리하게 되면 1초마다 렌더되는 부분이 전체가 아닌 타이머 컴포넌트로 줄어들었기 때문에 렌더 시간을 개선할 수 있습니다.다.
수치로 증명하자면 Render 시간이 14ms → 0.9ms로 줄어든 것을 볼 수 있습니다!
이렇게 동작 원리를 알게 되고 나서 코드를 보는 관점이 달라졌습니다.
코드만 보면서 이 부분은 렌더링이 많이 발생하겠다는 것을 알 수 있게 되고, 어떻게 최적화할 수 있을지 고민해 볼 수도 있습니다.
“나는 언제 이 컴포넌트가 렌더링 되는지 모르겠어.”, “기본기가 부족한 것 같아.”, “성능 최적화를 해보고 싶어.” 이런 생각을 가지고 계신 분들에게는 React의 동작 원리를 이해하고, 직접 구현해 보는 경험을 가져보시는걸 추천해 드립니다. 또한 4일 후에 3기 모집을 시작하고 있으니, 항해 플러스 프론트엔드 코스를 신청해보셔도 좋을 것 같습니다!
https://hanghae99.spartacodingclub.kr/plus/fe
이번 주 부터는 클린 코드에 대해서 배우게 되는데요. 다음 주에는 새로운 주제로 4주 차 후기를 들고 오도록 하겠습니다.
긴 글 읽어주셔서 감사합니다 :)
제가 현재 참여하고 있는 항해 플러스 프론트엔드 코스가 4기를 모집하고 있다고 하여 공유드립니다!
10월 24일까지 신청하시면 얼리버드 혜택으로 약 50% 할인을 받으실 수 있습니다.
또한 추천인 제도로 [추천인] 코드에 “zmK2OE”를 입력하시면 20만 원 추가 할인 혜택이 있으니
관심 있으신 분들은 항해 플러스 공식 홈페이지에서 신청하시면 됩니다 :)
저의 후기 글을 보시고 항해 플러스 프론트엔드 코스 관련해서 궁금한 사항이 있으시다면 링크드인 메시지 또는 이메일 yoosioff@gmail.com으로 편하게 연락주세요.