새해 맞이 블로그 이전합니다.
하테나가 좋다고 생각했는데, 이용하다보니 불편한 부분이 점점 늘어나서 더 이상 사용하기엔 좀 부담감이 큰 거 같습니다. 특히 SEO가 제대로 처리되지 않는 부분은 큰 감점으로 느껴졌습니다.
그래서, hashnode 블로그로 이전하고자 합니다. codelibre.hashnode.dev
새해 블로그부터는 개발 이야기도 할 거지만, 평소의 여러 생각들이나 일상들, 본래 취미였던 게임이나 애니 감상 등에 대해서도 이야기해볼 수 있으면 좋겠다고 생각하고 있습니다. 저쪽에서도 잘 부탁드립니다.
react-virtuoso를 이용한 테이블 최적화 (by Windowing)
👍 합시다, 최적화.
어드민 개발을 하다 보면 이미 만든 것에서 기능을 개선해달라는 요청을 받는 경우가 많다.
이번에 하고 있던 작업도 그랬다. 1차로 배포한 테이블 컴포넌트를 이용하신 분들께서 무한 스크롤 등의 기능이 좀 더 추가됐으면 하는 의견들을 주셔서 이를 고려해 개선 작업을 진행했다.
그러던 중, 내 코드를 매번 봐주시던 분께서 이번에 개선된 사항을 보시더니 테이블 최적화가 필요할 것 같다는 의견을 주셨다.
어떤 API든 가져와서 쓸 수 있는 구조인 만큼, 어떤 API에서는 데이터가 수 십~수 백개 정도만 받아질 수 있지만 어떤 API에서는 수 천, 수 만, 혹은 그 이상이 될 수 있으며 그 경우 분명한 성능 저하 이슈가 발생할 수 있다고 하셨다.
최적화… 개념은 듣기만 했지, 실제 케이스에 반영해 본 적은 없어서 어떤 식으로 최적화하면 좋을지 잘 모르겠다고 솔직하게 말씀드리니.. 그분께서 담백하게 말씀해주셨다.
그래요? 그럼 해봅시다, 최적화.
그리고 이렇게 테이블 최적화… 아니 그냥, 최적화 그 자체에 대한 작업을 처음 진행해보게 됐다.
💭 렌더링될 컴포넌트를 줄이거나, 혹은 렌더링될 횟수를 줄이거나.
그분이 제시해주신 최적화의 방향은 크게 위의 두 가지를 가리켰다.
물론 이외에도 다른 최적화 케이스(ex> 브레이킹 포인트를 설정한 이미지 최적화 등)가 있기는 하겠지만 흔히 말하는 React에서 최적화를 한다면 크게 저 두 가지를 가리킨다는 듯 하다.
확실히 렌더링될 컴포넌트가 늘어난다면 양이 점점 많아져서 DOM이 부담스럽다고 느낄 것이고, 렌더링될 횟수가 많아지면 유저가 input에 한 글자를 입력할 때마다 엄청 버벅이거나 하는 느낌을 줄 수도 있다.
이번에 진행할 최적화는 렌더링될 컴포넌트를 줄이는 방법이었는데, 여기서 처음으로 ‘윈도잉’라는 방법을 처음 알게 됐다.
애플리케이션에서 많은 리스트(수 백 또는 수 천행)를 렌더링하는 경우, ‘윈도잉(windowing)’이라는 기법을 사용하는 것을 추천합니다. 이 기법은 주어진 시간에 리스트의 부분만 렌더링하며 컴포넌트를 다시 렌더링하는 데 걸리는 시간과 생성된 DOM 노드의 수를 크게 줄일 수 있습니다.
react-window와 react-virtualized는 널리 알려진 윈도잉 라이브러리입니다. 리스트, 그리드 및 표 형식 데이터를 표시하기 위한 몇 가지 재사용 가능한 컴포넌트를 제공합니다. 애플리케이션의 특정한 활용 사례에 더 적합한 것을 원한다면 Twitter처럼 자신만의 윈도잉 컴포넌트를 만들 수 있습니다.
최근 공식 문서에는 없고, 이전 공식 문서에서는 성능 최적화에 대한 이야기를 언급한 내용이 있었는데 거기서 위와 관련한 내용을 볼 수 있었다.
이 중에 신경 쓰이는 단어가 두 가지 보였다. 바로 ‘리스트 가상화’와 ‘윈도잉’이다.
🤔 리스트 가상화(윈도잉)?
테이블이나 무한 스크롤이 적용되는 리스트 등의 경우, 데이터가 늘어날 수록 그 내용을 전부 화면에 그려주게 되는데, 이는 브라우저의 성능에 영향을 줄 수 밖에 없다.
위의 인용에서도 언급된 내용이지만, 이런 상황에서 React의 (구) 공식 문서에서는 윈도잉(Windowing)이라는 기법을 이용해 데이터 리스트를 가상화하라고 했다.
내용만 봤을 때에는 ‘윈도잉’이 ‘리스트 가상화’를 하는 하나의 방법이라고 생각했는데, web.dev의 글 등을 통해서 ‘리스트 가상화’ 또는 ‘윈도잉’이라고 언급되는 걸 보니 둘이 같은 단어를 가리키는 듯하여 여기서부터는 ‘윈도잉’이라고 말하겠다.
윈도잉이란, 한 마디로 사용자에게 보여지는 항목만 렌더링하는 기법이다.

이 기법을 이용하면 스크롤을 올리거나 내릴 경우, 보이지 않던 요소는 DOM 요소에 렌더링되면서 생겨나고, 안 보여지게 된 요소는 그대로 DOM 트리에서 빠지면서 렌더링되지 않게 된다.
이는 마치 스크롤을 움직일 때마다 리스트를 바라보는 창이 왔다갔다 움직인다고 하여, 이 기법을 윈도잉이라고 부른다.

✅ 그래서 선택한 React Virtuoso는?
사실 선택했기보다는 프로젝트에서 이미 이전 담당자가 React Virtuoso 라이브러리를 쓰고 있었다.
그래서 선택지가 별도로 있진 않았으나, 이왕 이렇게 된 거 위에서 언급된 react-window와 react-virtualized를 비교해봤다. 다만, 직접 하나씩 비교하기보다는 이미 다른 곳에서 위의 세 라이브러리에 대한 비교를 한 내용들이 있어서 그대로 가져와보고자 한다.
위의 이미지를 포함하여 개인적으로 생각하는 React Virtuoso의 장점들은 다음과 같았다.
- 우선 가장 활발하게 유지보수가 되고 있다.
- 제공해주는 가상화 타입이 다양한 편이다. (일반적인 리스트부터, 그리드, 그룹, 그리고 테이블 등)
- 실제 프로젝트에서 먼저 사용되고 있던 건 테이블이었다.
- 리스트의 높이에 대해서 가변적으로 대응해준다.
- 제공해주는 커스텀마이징 API 기능들이 다양하다.
- 실제로 사용할 때는 이 모든 내용을 다 알 필요도 없고, 사용도 하진 않았지만 제공해주는 것 자체가 이렇게 많다는 것이 큰 매력 요소로 느껴졌다.
- Fixed Header, Fixed Footer 적용이 가능하다.
- 이게 조금 중요했던 게, 사내의 디자인 시스템을 그대로 반영해야하기 때문에 없으면 안됐다.
- endReached 기능 등을 부여하여 테이블 마지막에 닿을 시에 발생할 수 있는 이벤트를 적용할 수 있다.
- React Native의 VirtualizedList와 같은 기능을 여기서도 볼 수 있다니…!!!
- 이를 이용해 무한 스크롤 등의 구현을 원만하게 적용해볼 수 있다.
위의 사항 외에도 프론트 진영에서 유명한 Tanstack에서 제공하는 Tanstack Virtual이 있지만, 테이블을 사용하려면 Tanstack Table도 설치하여 같이 적용해야 하므로 패키지를 두 개 설치해야하는 점과 Tanstack Virtual이 나온 지 얼마 안 된 점을 들어 여기서는 배제하기로 했다.
아무튼, 위의 사항만 고려해도 React Virtuoso는 굉장히 매력적인 라이브러리이다. 이 패키지가 아닌 다른 패키지로 대체하는 것에는 이유가 없다고 생각해서 기존에 사용중인 이 라이브러리를 이용해서 테이블 최적화를 진행해봤다.
🏁 그래서 React Virtuoso를 활용한 결과는?
위의 사항을 토대로 React Virtuoso를 이용해 테이블을 재배치했다.
export function Table({ list, columns, titles, fetchNextPage, isFetchingNextPage, hasNextPage }: Props) { return ( <TableVirtuoso style={{ width: '100%', height: 800 }} data={list} components={{ // 테이블 전체에 관련한 사항 적용 Table: (props) => { return ( <table {...props} /> ); }, // 테이블 각 열에 관련한 사항 적용 TableRow: (props) => { return <tr {...props} />; }, // 테이블 값이 비어있을 때에 관련한 사항 적용 EmptyPlaceholder: () => { return ( <tr> <td> {...내용 비어있을 때의 로직} </td> </tr> ); }, }} // 테이블 상단에 고정할 헤더 컴포넌트 fixedHeaderContent={() => ( <tr> {titles.map((제목) => ( <th key={제목}>{제목}</th> ))} </tr> )} // 테이블에 노출될 리스트 아이템들. 내용에는 없지만 기본적으로 tr을 감싼 채로 나온다. itemContent={(index, data) => columns.map((열내용) => ( <td key={열내용}> {data[열내용]} </td> )) } // 테이블 하단에 고정할 푸터 컴포넌트 fixedFooterContent={() => isFetchingNextPage && (<td>불러오는 중...</td>)} // 테이블 최하단에 도달 시에 적용될 이벤트 endReached={() => hasNextPage && fetchNextPage()} /> ); }
이 내용을 토대로 기존에 만들었던 무한 스크롤이 적용된 테이블과 위 내용으로 무한 스크롤이 적용된 테이블을 비교해보기로 했다.
CPU를 x6 감속으로 적용한 뒤, 데이터 8,512개(!)가 담긴 데이터를 무한 스크롤로 적용한 뒤 데이터를 처음 받아질 때부터 리스트의 마지막까지 도달하기까지의 시간과 성능을 측정해봤다.
- 변경 전 (3분 32초)

- 변경 후 (1분 8초)

위 결과에서 주목해야 할 점은 INP인데, 다음 페인팅까지의 인터렉션 시간을 비교하면 수치 자체가 확 달라짐을 느낄 수 있다.
또한, 리스트 마지막까지의 도달 시간도 무시할 수가 없는데 변경 전에는 스크롤을 내릴 수록 DOM에서 계산해야 하는 요소의 수가 점점 늘어나면서 8,512개의 데이터를 받아오는 총 시간이 3분을 넘겼다.
그러나 React Virtuoso를 적용한 변경 후에는 일정한 속도로 데이터를 받으면서 리스트 마지막까지 도달하는 데에 1분 정도가 걸렸다.
👋 마치며
처음 수정 확인을 위해서 테스트했던 API는 300~400개 정도 되는 리스트였고, 거기서는 체감이 별로 되지 않아서 ‘정말 최적화한 게 의미가 있나?’ 라는 의문이 들었다.
하지만 몇 천, 몇 만 개가 있는 API를 통해 확인해보니 최적화 여부가 얼마나 큰 체감을 주는지 알 수 있었다. 하물며 CPU가 성능이 낮다면, 요 테이블을 사용하는 분들에게는 얼마나 더 크게 느껴졌을까?
이번 작업 덕분에 정말 손톱 만큼이라고 생각하지만 가상화와 윈도잉에 대해서 알게되기도 했고, 향후 최적화를 할 때 어떤 기준을 가지고 진행할 지에 대한 여부와 또 어떤 식으로 최적화를 해야할지를 깨닫게 되는 좋은 경험이었다.
다른 사람들도 혹여 테이블이 아니더라도 리스트에 대한 최적화가 필요하다면 한번 쯤은 윈도잉과 위의 라이브러리들을 활용해보는 것을 추천한다.
🔗 출저
P.S. 마크다운 줄 바꿈이 제대로 먹히지 않는 걸 보니 또 블로그 이전을 고민해봐야겠다.. 새해가 되면 note나 qiita로 옮겨보자..
2024년 여름 회고
😱 세상에 벌써 9월이라니
뭐 한 것도 없는데 내일부터 9월이라고 한다.
뭔가 이룬 것도, 한 것도 없는데 30대 초의 여름은 너무나도 순식간에 지나간 것 같다.
음.. 아니다, 정정하겠다. 뭔가... 뭔가... 많았던 것 같다.
개인 프로젝트를 열심히 만들기도 했고, 서류를 열심히 지원 넣기도 했고, 면접도 보고, 공부도 하고, 병원도 다니고.. 한 것은 참 많았다.
다만, 결실을 맺은 것이 하나도 없었던 것 같다.

오랜만에 회고를 쓰고 싶어서 써봤는데 다 쓰고 보니 찡찡거리는 내용 밖에 없더라...ㅋㅋㅋㅋ...
그럼에도 쓰는 것은 올 여름은 정말 정신적으로도, 육체적으로도 힘들었다.
그래서 글로나마 속 안의 응어리를 찡찡대는 것으로 덜어내고자 써보기로 했다.
🥶 날씨는 더운데, 서류 결과는 차갑다
여름은 내내 서류를 넣고, 회사를 면접다니는 시간의 연속이었다.
잡코리아에서 서류를 넣고, 없으면 사람인에서 서류를 넣고, 없으면 원티드에서 서류를 넣고, 없으면 잡플래닛에서 서류를 넣고, 없으면 점핏에서 서류를 넣고, 없으면 인쿠르트에서 서류를 넣고, 없으면 다시 잡코리아로… 도르마무의 연속이었다.
이 글을 작성할 때에 한 달에 얼마나 서류를 넣었는지 확인해보니 한 달 평균 50개는 넣고 있었던 듯 하다.
면접은 요 여름 동안 10회 내외로 있었는데, 결과는 모두 불합격이었다😇

쓴 고배를 계속 마시게 되니, 점점 여러 생각이 들기 시작했다.
내가 지식이 부족한 게 문제인 걸까, 혹은 면접 태도가 문제였을까.
아니면 정말 운이 좋지 않은 거였을까, 시기가 좋지 않았던 걸까..
‘그래도, 면접까지 간 것이 어디냐’와 ‘이렇게 또 면접 경험 또 쌓았다.’라는 생각으로 면접 탈락의 고배를 마실 때 스스로를 안심시켰다.
아니, 이렇게라도 생각해야 했다.
취업이 절실한 현 상황에 이렇게라도 생각하지 않으면 진짜 무너질 거 같았다.
😵 너무나도 힘들었던 6월과 7월, 그리고 다시 보게 된 ‘야구’
전체적으로 무너지기 시작한 시기는 무더위와 습함이 점점 커지기 시작한 6월 중순부터였다.
실업급여가 끝난 시점부터 어찌됐든 돈을 다시 벌어야 하는데, 임시로 아르바이트를 하기엔 언젠가 취업이 될 것이라는 근거 없는 믿음에 섣불리 하질 못했다.
거기에 직전 회사의 일로 인해 다시 해야지라는 생각이 돌아오지 못했다.
받지 못하는 200만 원이 넘는 월급은 아직도 감감무소식이었고, 그 짧은 한 달 간의 노력은 물거품이 되었으며, 고소를 지급하기엔 모두가 무력감을 느끼기도 했고, 간이대지급금은 회사 설립일로 인해 받지도 못해서 정말 아무런 수확도 거두지 못한 처지가 됐다.
그 충격 때문인지 뭔가를 하는 것도 쉽지 않았고, 갑자기 생각나면 가슴이 답답하고 홧병이 터져서 견디기가 어려웠다.
그래서 한동안은 침대에서, 방에서 나가지 못하고 처박혀있기만 했던 것 같다.

거기에 알게 모르게 들린 나를 향한 외부 사람들의 불쾌한 이야기와 집에서도 밖에서도 신경쓰이는 부분들이 발생하니까 진짜 견디기가 너무 힘들었다.
그래서 이런저런 일들이 겹치다 보니 멘탈 케어를 위해서라도 잠시 동안 휴식이 필요하겠다 싶었고, 모든 SNS를 다 지우고 멘탈 회복에 집중하기 위한 시간을 가지기로 했다.
그렇게 아무 것도 안하고 진짜 연락도 멀리하고 보낸 한 달은 완전 회복은 아니더라도 어느 정도의 디톡스 효과를 좀 봤다.
다만 그 기간 중에 많은 것을 지워버리니 주변이 조용해져서, 부정적인 생각으로부터 생각을 돌릴 것이 필요했는데... 그 때 우연히 ‘최강야구’를 보게 됐다.

‘최강야구’라는 프로그램은 여러 가지 이유로 은퇴하거나, 또는 나와 같이 취업을.. 즉 프로로 가기 위한 선수들이 야구를 하고 싶어서 모이고, 그래서 여러 고등, 대학, 독립 리그 팀들과 야구 팀으로서 경기를 하는 프로그램이다.
우연히 봤던 프로그램은 등장했던 한 사람, 한 사람의 절실함과 노력에 몰입하며 보게 됐고, 많이 울면서 방송을 통해 어딘가에서의 위로감을 느꼈다.
너무나도 어렵고 힘들고 각박한 현실에라도, 열심히 하는 사람들이 보여주는 그 모습이 아무래도 많은 위로가 되었었나 보다.
그렇게 우연히 보던 프로그램은 매주 월요일마다 찾아보는 프로그램이 되었고, 자연스럽게 이 프로그램을 보다 보니, 다시금 예전에 좋아했던 ‘야구’를 직접 보고 싶다는 생각이 들었다.
그래서 어릴 때부터 응원했던 팀인 ‘LG 트윈스’ 경기를 찾아보기 시작했는데, 요즘도 야구를 하는 날이면 LG 경기를 라디오처럼 옆에다 틀어놓고 귀로 들으면서 작업하고 있다.
여담이지만 LG 선수들 내에서는 각별히 좋아하는 선수가 한 명 있는데, 바로 불펜 수호신 김진성 선수이다.
NC에 있던 이 선수가 방출이 되면서 모든 구단에 연락을 돌렸다가 LG에 연락을 했을 때 차명석 단장이 이 선수를 ‘김진성이니까 와라’라고 하면서 데려갔다는 이야기는 야구팬들 사이에서는 워낙 유명한 이야기이다.
그리고 이렇게 LG 트윈스에 들어간 노익장은 불펜의 수호신이 되어서 23-24 시즌의 1이닝을 책임지는 사람이 되었다.

김진성 선수는 인터뷰를 할 때 항상 하는 말이 ‘매 순간, 매 상황이 마지막이라고 생각하고 최선을 다한다’고 한다.
은퇴를 생각할 수 있는 나이임에도 노력해서 야구가 하고 싶어 자신을 어필하고, 어렵게 들어간 구단에서 자신의 모든 것을 쏟아부어 매 경기 세이브를 이뤄내는 이 선수의 모습에 너무나도 감명을 받고 기운을 얻었다.
조만간 야구 직관을 가고 선수 옷도 구입하고 싶은데, 등번호를 새긴다면 첫 번째는 무조건 빛진성으로 해야겠다고 생각했다.^^
이렇게 야구는 어느 새인가 오랜만에 내 일상에 스며들어왔고, 물론 요즘 나에게 분노를 주는 존재지만... 그래도 야구 덕분에 우울감에서 해방될 수 있었다.
그런 의미에서 LG 트윈스가 올해도 우승했음 좋겠다. (안 될 것 같지만… 쓰읍…)
🤒 아프다, 아프다, 또 아프다.
이렇게 멘탈이 괜찮아졌을 즈음에, 다시금 본격적인 취업 준비를 하기 위해 구글 스타트업 캠퍼스를 오고가면서 개인 프로젝트 작업과 이력서 지원을 지속적으로 하고 있던 와중, 어느 날 잠을 자려고 하는데 등골이 오싹해지는 느낌이 들었다.
딱 봐도 몸살 기운이라는 게 느껴졌고 약을 먹고 누웠으나 이게 감기로 인한 몸살이 아니라는 걸 알기까지 오랜 시간이 걸리지 않았다.
장염이 와버렸다. 그것도 엄청 심해서 아무 것도 먹지도 못하고 아무 것도 하지 못 할 정도의 장염에 걸려서 1주일을 내내 침대에서 보냈다.
이 시기에 정말 서러웠는데, 위에서 말한 어려웠던 시기에서 이제 겨우 회복해서 좀 다시 시작하려고 노력했더니 아무 것도 못 할 정도로 장염에 걸려 무기력하게 되어버리니 뭘 할 엄두가 나지 않았다.
진짜 일주일 내내 나는 왜 이래야 하나 별별 생각도 들고 서러워서 며칠은 내내 울었던 거 같다.

그렇게 아픔을 딛고 지낸 6월 말과 7월 초를 지나 이제 당분간 안 아프겠지 생각한 현재의 8월...
염병할 코로나가 내 몸을 한 번 거쳐갔다.ㅋㅋㅋㅋ.....
다만, 8월에는 멘탈 상태가 괜찮아서 그런가 ‘어, 아프네?’라고 생각만 했고, 대수롭게 여긴 걸 보니 새삼스럽지만 ‘멘탈 상태’의 중요성을 깨닫기도 했다.
💪 그리고 다시 돌아온 트위터, 진전이 나간 개인 프로젝트
그래서 위에서 저렇게 이야기하고 요 여름에 그래서 한 게 없느냐 물으면 그렇진 않았다.
먼저 개인프로젝트 이야기를 좀 해보자. 위의 일들이 있고 나서, 오랜만에 본 개인 프로젝트의 코드는 그야말로 끔찍하다는 느낌이 가득했고 전체적으로 엎어야겠다는 결심을 했다.
그래서 Vite와 React 기반의 프로젝트를 Next.js App Router 기반의 프로젝트로 바꿨는가 하면, 한편으로는 여태까지 짜놨던 코드 방식들이 다 마음에 안 든다고 코드를 리펙토링하고 그러다보니 아예 새로 만든 프로젝트처럼 되어버렸다.

사실상의 리빌딩을 통해 깨달은 건 내가 TypeScript를 되게 지협적으로 쓰고 있었으며, 전역 상태 관리도 그냥 썼지 왜 써야하고 무엇이 좋은지는 생각해보지 않았어서 이번 리빌딩을 통해 그런 부분들을 좀 깨달을 수 있는 기회가 됐다.
그리고 다시금 스스로 지향하는 ‘사이드 프로젝트 주도 개발’에서 추구하는 것이 무엇인지도 깨달았다.
현재 그 개인 프로젝트는 몇 가지 기능을 추가하는 것으로 테스트 런칭까지 올 수준이 왔다.
올해 안에 본격 런칭이 목표이므로 조만간 트위터 등으로 공개하는 일이 있을 것 같다.
아, 트위터 이야기가 나와서 말인데 위에서 말한대로 모든 것을 삭제하고 야구를 보며 지내다 좀 괜찮아져서 돌아오려고 했더니… 한 달이 지나 있었다, Oh…

그래서 결국 새 계정을 팠는데, 오히려 처음으로 돌아와서 그런가 마음이 편했다.
보고 싶은 거만 보고, 하고 싶은 말만 하는 계정이 되기도 했고…
사실상 개발 계정이 아니게 된 것 같으니 그냥 이쪽을 추구하기로 했다. 오히려 좋아^^
✅ 마지막 근황, 사이드 프로젝트와 단기직 근무 시작
아참, 그래서 이렇게 줄줄 이야기했지만 결국 마지막 이야기를 안 했는데..
우선 또 다른 사이드 프로젝트를 진행하기로 했다. 작년 너굴콘을 통해서 연사자로 연이 닿게 된 한날 님을 통해 토이 프로젝트 모임에 참가하게 됐다.
시작한 지 얼마 되지 않았는데, 얼마 전까지는 사실 이걸 진행하면서 느꼈던 고민으로 인해 어떻게 할지 망설이다가 며칠 전 한날 님에게 이야기를 드렸는데 깊은 말씀을 해주셔서 반성하고 다시금 집중해보려고 한다.
프로젝트가 잘 될 지는 모르겠지만, 적어도 프로젝트를 하시는 분들께 누를 끼치지 않기 위해 노력해야겠다. (하지만, 어제 실수를 일으켜버렸지만,,,🤦)
그리고 며칠 전에 모 회사에 단기계약직 프론트엔드 개발자로서 지원하게 됐는데, 라이브 코딩에서 사실상 망쳤기 때문에 떨어졌구나 해서 신경쓰지 않고 있었다.
근데 단기계약직으로 일해보지 않겠냐고 제의가 들어왔고, 당장의 돈도 없는 데다 일 경험이 너무나도 중요하다고 생각해서 제의에 승낙해 며칠 전부터 단기계약직으로 일하게 됐다.

현재 하는 일은 백오피스 근무를 하고 있는데, 이런저런 잡음이 있긴 하지만 첫 1주일은 예상했던 것과 좋은 의미로도, 나쁜 의미로도 달랐고 아직까지는 괜찮다고 생각하고 있다.
무엇보다 함께 일하시는 직원분과 이런저런 이야기를 하면서 짧은 기간임에도 많은 개발 이야기를 나누게 됐는데, 이런 부분에서 만족감을 엄청 크게 느끼고 있다.
수습이 1개월이기 때문에 열심히 하려고 노력하는데, 어찌저찌 잘 마치고 기회가 된다면 연장도 해서 계약 기간 동안 이것저것을 좀 가져가고 싶다.
👟 끝으로, 가을의 목표
우선 당분간은 취업을 생각하지 않기로 했다.
솔직히 말하자면 지쳤다는 게 가장 컸고, 계약직이지만 여기서 해볼 수 있는 경험을 3개월 쌓고 싶다는 욕심도 컸다.
주변에서 들었던 이야기도 있었기에 업무에 대한 기대가 크진 않았는데, 생각보다 괜찮아서 만족하고 있기 때문에 천천히 여기서부터 다시 빌드업해서 재취업을 준비하고자 한다.
그 대신, 사이드 프로젝트에 관한 사항을 집중해보려고 한다.
우선 한날 님이 주선해주신 푸딩 캠프의 토이 프로젝트 모임에 집중하면서 프론트엔드 한정으로 자그만한 PM 역할을 해야할 것 같다. (안 그러면 일이 안 돌아갈 것 같다…)
그리고 여기서는 자동화에 대해 공부해보고 싶었는데 이 기회를 살려서 Github Action을 가지고 여러가지 시도를 해보고 이를 문서화하거나 블로깅하려고 한다.
위에서 언급했던 개인 프로젝트 역시 천천히 작업하고자 한다.
8월 런칭 목표라고 했는데, 또 질질 미루게 됐지만 기능 구현도 얼마 안 남았으니 틈틈히 시간을 내어서 작업해서 테스트 런칭을 가을 안에는 이뤄내고 싶다.
마지막으로 하루 하나 개발글 읽기를 습관화해보고자 한다.
단기 계약직으로 일하기 시작한 날부터 하루 하나 개발 아티클 읽기를 하고 있다.
내용이 이해가 되든 안되든 일단 뭔가를 쑤셔넣어야 나중가서 ‘아, 그 때 그거!’라고 생각했던 경험이 많았는데, 출퇴근 시간을 이용하여 다른 사람이 쓴 공부한 내용이나 혹은 개발 아티클을 읽으면서 하나씩 머리에 정보를 담아가려고 한다.
요즘 관심 있는 건 TypeScript의 General, 함수형 프로그래밍, 그리고 Tanstack-query인데 조만간 이에 대해서도 블로그로 정리할 시간이 있으면 좋겠다.
아무튼 무기력한 시기를 벗어나 우연히 찾아온 기회와 함께 가을을 맞이했다.
‘여전히 나는 취업을 할 수 있을까?’, ‘계약직 수습 잘 할 수 있을까?’ 하는 막막함도 드는 등 잡생각이 드는 요즘이지만 그래도 열심히 하다보면 뭔가 어떻게든 되지 않을까.
여름에 많은 것들이 힘들었던 만큼, 이런 것들이 액땜이 되어서 앞으로 좋은 일만 가득했으면 좋겠다.
아무튼 힘내자, 매번 화요일에 지더라도 다시 승리를 위해 달리는 LG 트윈스처럼!

Code Push 적용기 in React Native 0.74 - (2) 이제 Code Push를 연결해보자

App Center 연동은 끝났고, 이제는 Code Push를 해보자.
저번 글에서는 App Center의 연동을 끝냈다.
이번 글에서는 Code Push 패키지를 설치하고 배포까지 하는 과정을 작성해보고자 한다.
참고로 작성자는 Window OS에서 개발을 진행하고 있다.
따라서 iOS 내용은 제외되어 있으며, 여기선 Android 내용만 다룬다.
또한, 작성일 기준으로 최신 버전인 React Native 0.74.1에서는 Code Push가 호환되지 않는 이슈가 있는 듯 하다.
(한동안 0.74.1 버전에서 적용하도록 실험해봤으나, 아래의 이슈가 발생하고 있으며 이건 호환성 문제로 보여지고 있다.)
궁금한 사람들은 여기를 누르면, 관련 이슈를 볼 수 있다.
따라서 여기서는 React Native 0.73.8에서 진행한 내용을 다루고 있으니, 자신이 개발중인 버전과 참고하여 읽길 바란다.
자, 그러면 Code Push 세팅을 해보자.
Code Push를 이용하려면 우선 Code Push 패키지를 설치해야 한다. 아래의 커멘드를 입력해 Code Push를 설치해주자.
// npm으로 설치 npm install react-native-code-push // yarn으로 설치 yarn add react-native-code-push
설치가 완료됐다면, (프로젝트 폴더명)\android\settings.gradle 파일을 열고 다음 내용을 추가해주자.
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
그 다음, (프로젝트 폴더명)\android\build.gradle에 다음 내용을 추가해주자.
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
여기까지 완료됐다면, MainApplication.kt 파일을 찾아서 일부 코드를 추가해줘야 한다.
React Native 0.73 버전부터는 위에 보이다시피 Kotlin으로 코드가 되어있다. (그 전까지는 Java 파일로 되어있다.)
그래서 자신이 보는 파일이 .kt가 맞는지 확인하고, 맞다면 아래의 내용대로 해주면 된다.
// 먼저 플러그인을 배치해준다.
import com.microsoft.codepush.react.CodePush
// 아래의 클래스 내 DefaultReactNativeHost 오브젝트에 일부 코드를 추가해주자.
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
...
// 아래의 코드를 추가해주자.
override fun getJSBundleFile(): String {
return CodePush.getJSBundleFile()
}
};
}
위의 과정까지 완료가 됐다면 (프로젝트 폴더명)\android\app\src\main\res\values\strings.xml에 다음과 같은 내용을 추가한다.
<string moduleConfig="true" name="CodePushDeploymentKey">(App Center에서 받을 수 있는 배포 키)</string>
배포 키를 확인할 수 있는 방법은 아래의 커멘드를 입력해서 확인할 수 있다.
appcenter codepush deployment list -a <ownerName>/<appName> -k
ownerName과 appName을 모르겠다면 Code Push를 연동하려는 App Center의 URL을 확인해보자.
https://appcenter.ms/users/<ownerName>/apps/<appName>
배포 키가 없다면 배포 키를 생성해줘야 하는데, 커멘드로 생성하는 방법과 App Center에서 직접 만드는 방법이 있다.
1) App Center에서 직접 만드는 방법
App Center 화면 -> Distribute -> Code Push -> Create Standard deployments 를 눌러 배포 버전들을 생성한다.

그러면 기본적으로 Staging과 Production, 이렇게 두 개의 배포 버전이 생성되는 것을 확인할 수 있다.

이제 VSCode로 돌아가 커멘드를 다시 입력하면 두 배포 버전의 배포 키가 생성되는 것을 확인할 수 있다.
이 생성된 키를 아까 (프로젝트 폴더명)\android\app\src\main\res\values\strings.xml에 배포 키 넣는 부분에 배치하면 된다.
2) 커멘드로 직접 생성하는 방법
아래와 같이 커멘드를 입력해주자.
appcenter codepush deployment add -a <ownerName>/<appName> <deployName>
deployName은 Staging, Production 등 위 내용에서 봤던 배포 버전을 가리킨다.
만들고자 하는 deployname을 적은 뒤 생성하면 key를 발급해준다.
이 생성된 키를 아까 (프로젝트 폴더명)\android\app\src\main\res\values\strings.xml에 배포 키 넣는 부분에 배치하면 된다.
이제 App.tsx의 App 함수를 CodePush 함수로 감싸주고 저장해주자.
import React from 'react';
import Naviagation from './src/routes';
import CodePush from 'react-native-code-push';
function App() {
return <Naviagation />;
}
export default CodePush(App);
여기까지 했으면 이제 남은 건 배포하는 것이다.
배포 전 Metro를 실행시키고 에뮬레이터 등에서 앱을 빌드해 실행해보자.
잘 된다면, 이제 마지막으로 아래의 커멘드를 입력해서 Code Push 버전을 배포하면 연결이 완료된다.
appcenter codepush release-react -a <ownerName>/<appName> -d <deployName>
위의 이미지처럼 Successfully released라고 나온다면 Code Push 배포가 완료된 것이다.
이제 App Center로 돌아가 Code Push 메뉴를 확인해보자.
아래처럼 버전이 추가됐다면 Code Push 연결 작업이 모두 완료된 것이다.
이렇게 Code Push 연결 작업을 완료해봤다.
0.74 버전에서의 Code Push 호환 문제는 추후 해결 방법이 있다면 별도의 글로 작성하도록 하겠다.
Code Push를 적용하는 데에 애먹는 분들에게 이 글이 조금이나마 도움이 될 수 있기를 바란다.
Trouble Shooting
Metro 실행 후, run Android로 에뮬레이터 실행을 시도해봤으나 codepush.gradle 파일을 찾을 수 없다는 이슈가 있었다.

왜 못 찾는지 의아하게 생각하다가, 나중에 유남주 님께서 알려주신 덕분에 codepush.gradle 파일을 찾는 경로가 잘못되어 있다는 것을 알게 됐다.
공식 문서에서 준 내용대로 가져와서 발생한 문제인데, 혹시 위의 이슈가 발생했다면, (프로젝트 폴더명)\android\build.gradle에 추가했단 apply 부분에 경로가 잘못되진 않았는지 확인해보자.
참고 자료
learn.microsoft.com github.com velog.io www.linkedin.com ingg.dev
Code Push 적용기 in React Native 0.74 - (1) 우선은 Microsoft App Center 연결부터

Codepush.. 할 줄 알아야 해요?
React Native 관련 채용 공고를 보면 열에 다섯은 보이는 내용이 한 가지 있다.
- CodePush 등을 이용한 효율적인 배포 관리 경험이 있는 분
- CodePush 등을 이용한 배포 관리 경험을 보유하신 분
- CodePush 사용경험
생각해보니 React Native으로 앱 기능을 구현한 경험은 있는데 앱 배포 담당이 따로 있던 탓에 Code Push를 사용해 본 경험이 없었다.
그래서, 이 기회에 Code Push를 적용해서 앱 배포 및 관리를 해보는 과정을 작성해보기로 했다.
Code Push가 근데 뭔가요?
CodePush는 React Native 개발자가 모바일 앱 업데이트를 사용자의 디바이스에 직접 배포할 수 있도록 하는 App Center 클라우드 서비스이다.
보통 앱을 배포할 때에는 항상 Android나 iOS 모두 심사를 거쳐서 업데이트 내용을 배포해야 하는데, 이 경우 시간이 오래 소요된다.
하지만 UI 요소나 스타일링, JavaScript, 이미지 등의 요소들만을 수정한 것뿐이라면 Code Push SDK를 이용해 앱 심사를 거치지 않고 사용자의 앱에 바로 배포할 수 있다.
또한, 이렇게 배포한 사항들을 배포 단계 및 빌드 버전으로 나누고 관리할 수 있다는 점도 큰 특징이다.
다만, 여기서부터는 좀 중요한데 이 Code Push를 관리하는 Microsoft App Center는 내년 3월까지 운영될 예정이다.
이런 상황임에도 불구하고 아직 많은 곳에서 Code Push를 통한 앱 빌드 및 배포, 관리를 하고 있다 보니, App Center가 사라지면 Code Push를 어떻게 할지 많은 사람들이 우려했다.
다행이라면, Code Push 기능은 독립적인 방식으로 이용할 수 있도록 조치하겠다고 Microsoft에서 안내하고 있다.
이 점을 고려해 Code Push를 적용하는 글을 작성하고 있지만, 혹시 모를 사항을 대비해 대체제를 고려해볼 필요는 있다고 생각한다.
그에 대한 내용은 추후 다른 글에서 다뤄보도록 하고, 여기서는 Code Push를 적용하는 과정 만을 기록하고자 한다.
우선 App Center 등록부터 해보자.
Code Push를 이용하기 위해서는 앞서 말한대로, Microsoft App Center에 계정을 등록해야 한다.
여기를 눌러 App Center 계정을 등록해보자.
가입을 진행하면 username을 입력하라고 나온다.
이 이름은 나중에 Code Push를 연결할 때 필요해지므로 잘 지어놓도록 하자(?).
완료하면 이제 다음은 App Center에 관리할 앱을 등록해야 한다.
Add New App 버튼을 눌러서 App에 관한 정보를 설정해주자.
App Name을 작성하고, OS는 Android로, 그리고 Platform은 React Native를 선택하고 Add New App을 누르면 아래와 같은 화면이 뜰 경우, 정상적으로 앱 등록이 완료됐다.
이 창을 끄지 말고 유지한 채로, 에디터를 키고 App Center CLI를 설치해서 로그인해보자.
아래의 커멘드를 터미널에 입력해서 설치해주자.
npm install -g appcenter-cli
설치가 완료되면 아래의 커멘드를 입력하고 엔터를 누르자.
그러면 App Center 로그인 페이지가 열린다.
appcenter login
로그인하면 인증 완료라고 하면 token을 알려주는데, 이 토큰을 복사 후 커멘드 쪽에 Access code from browser:라고 입력하라고 있는 쪽에 붙여넣기를 해주고 Enter를 눌러주면 App Center CLI와 연동이 완료된다.
여기까지 됐다면 이제, 아까 전의 App Center 웹 화면으로 돌아가 Getting Started에 나온대로 App Center와 연관된 패키지들을 추가로 설치해주자.
// npm일 경우 npm install appcenter appcenter-analytics appcenter-crashes --save-exact // yarn일 경우 yarn add appcenter appcenter-analytics appcenter-crashes
설치가 완료됐다면 (프로젝트 폴더명)/android/app/src/main 경로에 assets 폴더를 생성한 뒤, appcenter-config.json 파일을 생성한다.
생성된 파일에는 아래와 같이 내용을 작성한다.
{
"app_secret": "여기다 app secret를 입력한다. app secret는 App Center 처음에 Overview에서 Getting Started의 3번 내용을 보면 알 수 있다."
}
여기까지 완료됐다면, (프로젝트 폴더명)\android\app\src\main\res\values\strings.xml에 아래의 내용을 추가하고 저장하자.
<string name="appCenterCrashes_whenToSendCrashes" moduleConfig="true" translatable="false">DO_NOT_ASK_JAVASCRIPT</string> <string name="appCenterAnalytics_whenToEnableAnalytics" moduleConfig="true" translatable="false">ALWAYS_SEND</string>
이렇게까지 하면 App Center 연동 작업은 완료가 됐다.
다음 글에서는 본격적으로 Code Push 패키지 설치와 그 연동 작업을 진행해보기로 하겠다.
참고 자료
React에서의 렌더링 과정, 그리고 Fiber
📄 머리말
React를 공부해 본 사람들이라면 가상 DOM(Virtual DOM)이라는 이름을 들어본 사람은 적지 않을 것이다. 가상 DOM을 통해 우리가 React에서 수정한 내용들이 담겨지고 이를 실제 DOM에 반영한다는 건 나도 그렇고, 많은 사람들이 아는 이야기인데.. 하지만, 정작 그 과정에 대해 알아보려고 하진 않았던 것 같다.
그래서 이번 글에서는 기존 DOM에서의 렌더링부터, 가상 DOM, 그리고 이 가상 DOM을 이용한 React에서의 렌더링에 대해 공부한 내용을 정리해봤다.
이번에 공부한 내용을 모던 리액트 Deep Dive 책을 참고해서 공부했다.
부족하거나 잘못 정리한 내용이 있다면 언제나 댓글로 감사히 받고 참고하겠다.
📶 브라우저에서의 렌더링 과정에 대해서 잠시 짚고 가보자.
우선 DOM에 대해서 한 번 짚고 가자.
DOM은 Document Object Model의 약자이며, 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.
브라우저에서는 이 DOM을 이용해서 화면을 구상하고, 배치하고, 그려낸 뒤 이용자에게 보여준다.
- 브라우저가 사이트의 HTML 파일을 다운로드 받는다.
파싱 단계 - 브라우저에서는 HTML을 파싱하고, 각 노드로 구성된 트리를 만든다. (DOM)
DOM 트리 구축 단계 - 파싱 중 CSS 파일을 발견하면 해당 파일을 다운로드하고, 이 역시 CSS 노드로 구성된 트리를 만든다. (CSSOM)
CSSOM 트리 구축 단계 - 파싱이 완료되면 완성된 DOM 트리를 최상위인 html부터 최하위 노드까지 순회하여 화면에 보여지지 않는 요소들(
dispaly:none)은 배제하고 그려질 노드들만 렌더 트리로 구축한다.렌더 트리 구축 단계 - 이후 구축이 완료된 렌더 트리를 기준으로 똑같이 html부터 순차적으로 CSS 스타일 요소를 적용하기 시작한다.
렌더 트리 배치 단계- 과정은 화면에 어디에 배치할지를 계산하는 레이아웃부터 진행된다.
- 이후, 레이아웃이 끝나면 색을 입히는 과정인 페인팅이 진행된다.
- 이 과정까지 완료 되면 최종 출력물인 웹 페이지가 브라우저에 노출된다.
렌더링 단계
해당 과정을 도식화하면 아래와 같이 보여질 수 있다.


이후 화면에 그려진 다음 상호작용 과정 등이 발생하면 리플로우나 리페인팅이 발생하게 된다.
- 리플로우(Reflow)
- 특정 레이아웃 등의 변화로 인해서 배치가 변경되거나 크기가 수정됐을 경우 레이아웃 계산을 다시 하게 되는데 이 과정을 리플로우라고 가리킨다.
- 레이아웃이 변화하게 되는 만큼, 리플로우 이후에는 리페인팅도 다시 일어난다.
- 리페인팅(Repainting)
- 위와 같이 리플로우가 일어나거나, 또는 특정 상황에 의해서 요소의 색상이 변경되거나 할 경우에 화면에 다시 색을 입히거나 변경해주는 과정을 진행하게 되는데 이를 리페인팅이라고 부른다.
- 위와 같이 리플로우가 일어나거나, 또는 특정 상황에 의해서 요소의 색상이 변경되거나 할 경우에 화면에 다시 색을 입히거나 변경해주는 과정을 진행하게 되는데 이를 리페인팅이라고 부른다.
브라우저는 위와 같은 과정을 거쳐서 웹 페이지를 렌더링하게 된다.
이해가 되지 않는다면 요소가 간단히 있는 페이지에서 네트워크를 느리게 하고 다운 받는 파일들을 확인해보면 html을 먼저 받고, css 파일을 그 다음에 받는 것을 확인할 수 있다.

이렇게 브라우저에서 말하는 렌더링 과정에 대해서 살펴봤다.
그렇다면 React에서 말하는 렌더링은 무엇을 가리키는 걸까?
🎨 React에서 말하는 렌더링이란?
React에서 말하는 렌더링은 쉽게 말하자면 우리가 말했던 가상 DOM(Virtual DOM)을 이용해서 변경된 사항들을 실제 DOM에 반영하는 과정이라고 말할 수 있다.
좀 더 깊게 말하자면, id=”root”를 기준으로 생성되는 React의 어플리케이션 트리 안에 모든 컴포넌트들이 현재 자신들이 가진 props와 state를 기반으로 UI를 구성하고, 이를 계산해 DOM에 반영하는 과정이다.
React에 대해서 가볍게라도 공부해본 사람이라면 React에서의 렌더링은 크게 두 가지를 가리킨다. 바로 초기 렌더링과 리렌더링이다.
- 초기 렌더링
- React의 어플리케이션을 처음 진입하면 보여져야 할 내용들을 그려줘야 볼 수 있다. 이 과정을 초기 렌더링 혹은 최초 렌더링이라고 부른다. (이 글에서는 초기 렌더링이라고 부르겠다.)
- 리렌더링
- 위의 초기 렌더링을 제외하고 일어나는 React 어플리케이션의 렌더링 방식은 리렌더링이라고 부르고 있으며, 리렌더링이 일어나는 이유는 여러가지가 있다.
- React에서의 State가 변경되는 경우
- 이는 useState의 코드 속에 있는
render()라는 함수를 통해서 렌더링을 일으키게 되는데 요 부분에 대해서는 추후 useState의 과정을 살펴보는 글을 쓰게 되면 다뤄보도록 하겠다.
- 이는 useState의 코드 속에 있는
- key props에 변화가 발생한 경우
- 여러 수많은 요소들을 배치해야 하는 상황일 때 map() 메소드를 통해서 요소를 배치해 본 경험이 있을 것이다. 이때 이 메소드에 배치되는 요소에 key prop을 적용해야 한다는 경고를 본 적이 있을 텐데, 이런 key props에 변화가 생길 경우에도 리렌더링이 필연적으로 일어나게 된다.
- 부모 컴포넌트가 렌더링 되거나 props가 변경된 경우
- 부모 컴포넌트에 렌더링이 발생했다면 부모 컴포넌트가 포함하고 있는 자식 컴포넌트 또한 필연적으로 렌더링이 일어날 수 밖에 없다.
- 또한 props로 받아오는 값들은 전부 부모 컴포넌트를 통해서 받아오는 값들이다.
해당 값들이 변한다는 건 필연적으로 자식 컴포넌트에도 변화를 줘야하는 상태이므로 리렌더링이 일어나게 된다.
이처럼 React에서 말하는 렌더링이 무엇인지, 그리고 렌더링이 어떻게 구별되는지도 알았다. 그러면 대체 어떤 식으로 렌더링이 일어난다는 걸까?
렌더링을 하는데 가상 DOM을 어떤 식으로 이용하여 실제 DOM에 반영하게 된다는걸까?
📝 가상 DOM이 대체 뭐고, 어떠한 장점이 있는 걸까?
복잡하기 짝이 없는 브라우저에서의 렌더링 과정은 변화가 발생할 때마다 리플로우나 리페인팅이 계속해서 일어날 수 밖에 없는데, 당연히 이런 과정이 일어날 때마다 화면을 다시 배치하거나 그리는 비용은 필연적으로 커질 수 밖에 없다.
심지어 애플리케이션의 상호작용이 늘어난 요즘을 생각하면 그 비용은 이전보다 더 엄청날 것이다.
이런 과정에서 React를 만든 사람들은 이 문제를 해결하기 위해 고민했다.
‘어떻게 하면 이런 비용을 줄일 수 있을까?’라는 물음을 던진 그들은 곧이어 React에서 활용되는 가상 DOM을 만들어내게 된다.
가상 DOM(Virtual DOM)이라는 건 단어 그대로, 가상의 DOM이라는 의미인데 실제 브라우저의 DOM을 본따서 React에서만 사용되고 관리하도록 만든 DOM의 복사본이다.
React는 실제 DOM 데이터를 메모리에 저장하고, React에서 가상 DOM에서 변경 사항을 모두 반영했을 때 메모리에 저장해둔 DOM 데이터를 불러온 후 변경된 사항을 반영하고 이를 브라우저에 적용되도록 처리한다.
(이 때의 React는 React 패키지를 설치하면 함께 설치되는 react-dom을 가리킨다.)
이 방식을 거치면 리페인팅이나 리플로우가 일어날 때마다 DOM에 계속해서 반영되는 식이 아니라 React의 가상 DOM에서 모든 계산을 한꺼번에 다 처리하고, 그 처리가 완료되면 메모리에 저장된 DOM 데이터를 가져와 변경된 내용을 적용하기 때문에 계산 비용이 줄어들 뿐만 아니라 브라우저의 렌더링 과정을 최소화할 수 있다는 장점이 있다.
🧵 가상 DOM은 어떻게 만들어질까? - React Fiber
가상 DOM이 어떤 건진 위의 내용을 통해 이해가 됐다.
그렇다면 가상 DOM은 어떻게 만들어지고, 어떤 과정을 통해 DOM에 적용하는 비용을 최소화할 수 있는 걸까?
이를 위해서 알아야 할 친구가 하나 있다. React Fiber라는 녀석이다.
React 톺아보기 - 05. Reconciler_5 | Deep Dive Magic Code
React 톺아보기 - 05. Reconciler_2 | Deep Dive Magic Code
(좀 더 자세하게 알고 싶은 사람은 위의 글을 정독해보길 바란다.)
- React Fiber?
React Fiber란 React에서 관리하는 자바스크립트 객체이다.
React에서는 가상 DOM과 실제 DOM을 비교해 변경 동향을 파악하고 둘에 차이가 존재한다면 변경 사항을 관리하는 이 Fiber를 이용해서 화면에 렌더링을 요청하게 된다.
이처럼 렌더링 과정이 실행되면서 각 컴포넌트의 렌더링 결과들을 수집하고, 기존의 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경 사항을 차례대로 수집하는 과정을 재조정(Reconcilation)이라고 부르며, Fiber는 이런 재조정 과정을 관리해주는 재조정자(Reconciler)라고 부르기도 한다.
React는 이 Fiber를 이용해 여러 인터렉션에서 발생하는 반응 이슈를 해결하고 올바른 결과물을 화면에 렌더링하고자 했다.
그럼 React Fiber는 어떻게 이루어져 있을까?
function FiberNode( this: $FlowFixMe, tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; . . . this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.alternate = null, . . . }
위와 같이 생겼는데, 사실 이보다 내용은 더 많지만, 여기서는 일부 내용만 다루겠다.
tag- Fiber가 어떤 것을 담고 있는지를 분류해주기 위한 말 그대로의 태그다. 여기서 이 태그는 HTML의 DOM 노드일 수도 있고, 다른 것일 수도 있다.
key- 각 요소별로 가지고 있는 고유한 key를 가리킨다. 아까 리렌더링에서 이야기할 때 언급했던 key가 바로 이 key를 가리킨다.
stateNode- 해당 Fiber가 참고하게 되는 실제 요소나 컴포넌트를 가리킨다.
- 예를 들어
<div id=”root” />라는 요소와 관련한 Fiber에서의 stateNode는div#root를 참고한다.
child,sibling,retrun,indexchild는 해당 Fiber가 가지고 있는 하위 Fiber를 가리킨다.- 여기서 한 가지 알고 있어야 할 것은 React의 모든 컴포넌트나 DOM 노드 등은 하나의 Fiber 객체를 부여받으며 이 Fiber는 가장 먼저, 첫 번째 Fiber를 가리킨다.
sibling은 자식 Fiber나 같은 depth의 Fiber와 관련하여 다음에 올 Fiber를 가리킨다.return은 이런 자식 Fiber를 담고 있는 부모 fiber를 가리킨다.index은 같은 depth에 있는 Fiber들 중에서 어떤 순서를 가지고 있는지를 나타낸다.

pendingProps,memoizedProps- 위에서 살펴봤던 props가 변경됐을 때 리렌더링이 일어난다고 했는데, 그 props를 관리하는 내용이 이 두 속성이다.
pendingProps는 전달받은 props가 렌더링이 일어나기 전에 배치되는 내용이다.memoizedProps는 렌더링이 완료된 후pendingProps가 옮겨질 내용이다. 렌더링 이후pendingProps가 여기로 옮겨진다고 생각하면 된다.
updateQueue- Fiber와 관련하여 필요한 작업들을 담아두는 Queue이다.
- 상태를 업데이트 한다던가, 콜백 함수를 실행시킨다던가 등의 작업들이 관리된다.
- Fiber와 관련하여 필요한 작업들을 담아두는 Queue이다.
memoizedState- 현재의 함수형 컴포넌트와 관련하여 Hook Function들이 여기에 저장된다.
alternative- 변경 전후의 Fiber를 비교하기 위해 필요한 값이다.
내용 중에 이야기를 했지만 1개의 Fiber에는 1개의 컴포넌트나 DOM 노드 등과 엮어져 있으며, 그리고 이런 Fiber는 state가 변경되는 등의 DOM의 변경이 필요한 상황에서 실행된다.
이런 Fiber는 key나 stateNode, tag 등을 통해 보이듯이 UI를 단순하거나 복잡한 값들을 이용해서 객체에 관리하고, 작성한 JavaScript의 코드에 맞게 표현되도록 적용되어 있다.
이런 Fiber를 통해 React가 표현하고자 하는 것은 크게 세 가지이다.
- 각 컴포넌트나 DOM 노드 들을 1개의 Fiber로 단위를 쪼갠 뒤,
index나child,sibling을 통해 보이듯 우선 순위를 매겨서 적용할 작업을 진행한다. - 또한 필요에 따라서는 이러한 작업을 일시 중지하거나 나중에 다시 시작할 수 있다. (
pendingProps,memoizedProps) - 그 밖에도 이전에 했던 작업을 다시 재사용하거나, 필요하지 않은 경우 폐기하는 것도 가능하다. (
updateQueue,memoizedState)
그럼 React에서는 이 Fiber를 이용해 가상 DOM에서 어떤 방식으로 처리하고 DOM에 변화된 내용을 적용하는걸까?
🌳 Fiber를 연결하고 연결해 만들어지는 React Fiber Tree
React의 가상 DOM에는 Fiber로 구성된 2개의 Tree가 존재한다.
하나는 현재 유저가 보고 있는, 렌더링이 이미 된 상태의 화면 구성을 나타내는 current Tree이며, 다른 하나는 이후에 인터렉션 등을 통해서 변경될 사항들이 반영되어있는 workInProgress Tree이다.
React는 각 Fiber에서의 작업들이 끝나는대로 workInProgress Tree를 current Tree로 변경한다.
잠시 야구장에 있는 전광판을 생각해보자.
현재 나와있는 투수가 갑자기 교체되어야 할 상황이 발생하면, 전광판 시스템은 다음 투수가 누구인지를 받아내는 대로 그 정보들을 한꺼번에 정리한 다음 완료가 되면 아나운서의 소개 음성과 함께 관중들이 볼 수 있도록 전광판에 띄워준다.
이처럼 보이지 않는 곳에서 다음 그림을 다 그려낸 뒤에, 완성되면 그 그림을 현재 그림에서 바꾸는 방법을 더블 버퍼링이라고 하는데, React의 가상 DOM에서는 이 두 트리를 이용해서 각각 변화된 사항을 처리하고 있다.

이 과정을 이용해서 React는 업데이트가 발생하면 workInProgrss Tree에 새로운 데이터를 반영하고 빌드하기 시작한다.
그리고 빌드가 완료되면 다음 렌더링에 이 트리를 사용해 렌더링을 반영하게 되고, 그렇게 되면 workInProgress Tree의 내용이 이제 current Tree로 덮어씌워지게 된다.
좀 더 쉬운 이해를 위해 이미지를 보도록 해보자, 아래는 current Tree이다.

위 Tree에서 일부 컴포넌트에 변화가 발생했다고 하면, Fiber Tree는 아래의 그림처럼 workInProgress Tree에서 변화된 과정들을 추적하고 반영하는 작업을 하게 된다.

Fiber의 작업 순서는 크게 파이버 작업을 수행하는 beginWork(), 작업이 완료되면 파이버 작업을 끝내는 completeWork(), 그리고 모든 Fiber의 변경점이 반영됐을 시에 수행되는 commitWork()로 이루어진다.
위의 그림에서는 다음과 같은 과정으로 진행될 것이다.
<a1>에서beginWork()active<b1>에서beginWork()active, 별도의 자식이 없으므로completeWork()로 종료<b2>에서beginWork()active, 자식이 있으므로<c1>으로 이동<c1>서beginWork()active, 자식이 있으므로<d1>으로 이동<d1>서beginWork()active, 별도의 자식이 없으므로completeWork()로 종료<d2>서beginWork()active, 별도의 자식이 없으므로completeWork()로 종료<c1>의 자식에서 모든 수행을 완료했으므로completeWork()로 종료<b2>의 자식에서 모든 수행을 완료했으므로completeWork()로 종료<b3>에서beginWork()active, 자식이 있으므로<c2>로 이동<c2>서beginWork()active, 별도의 자식이 없으므로completeWork()로 종료<b3>의 자식에서 모든 수행을 완료했으므로completeWork()로 종료<a1>의 자식에서 모든 수행을 완료했으므로completeWork()로 종료- 최상위인
<a1>까지 모든 과정을 마쳤으므로completeWork()active, 업데이트가 필요한 사항이 DOM에 반영됨
이러한 과정을 거쳐서 가상 DOM에서 변경 사항들을 반영한 뒤, commitWork()까지 마쳐서 변경된 current Tree는 이제 DOM에 반영되도록 처리된다.
여기서 1번부터 12번까지, commitWork()가 작동되기 전 변경 사항들을 파악하는 단계를 Render Phase라고 부르며, 최종적으로 13번에서 변경 사항이 파악된 사항을 가상 DOM에서 실제 DOM으로 적용하는 과정을 Commit Phase라고 부른다.
참고로 Render Phase는 비동기적으로 진행되는 반면, Commit Phase는 모든 내용을 다 한 번에 반영해야 하므로 동기적으로 진행된다.
그리고, 앞서 말했던 재조정(Reconcilation)하는 과정이 바로 Render Phase에서 일어나게 된다.
마지막으로 현재의 Function Component 환경인 React Hook Cycle에서 Render Phase와 Commit Phase가 어느 부분에서 일어나는지 확인해보자.

💮 정리
지금까지 언급된 과정들을 요약해서 정리해면 다음과 같다.
- React에서의 렌더링이란, 실제 DOM을 본 딴 React에서만 사용되는 가상 DOM에서 각 컴포넌트에서 변경될 사항들을 수집하고, 계산하여 렌더링을 요청하는 재조정 과정을 거친 뒤, 이를 실제 DOM에 반영하는 과정을 말한다.
- React의 렌더링에는 변경 과정을 비동기적으로 체크하고 수집하여 요청하는 Render Phase, 그리고 Render Phase에서 만든 결과물을 실제 DOM에 반영하도록 해주는 Commit Phase로 진행된다.
- Render Phase에서는 React에서 각 컴포넌트나 DOM 요소를 관리하는 객체인 Fiber들로 모여진 Fiber Tree를 이용해 변경 전인 current Tree를 기반으로 변경된 사항들을 모아놓은 workInProgrss tree를 구축한다.
- Commit Phase에서는 구축이 완료된 workInProgress Tree를 실제 DOM에 반영하면서, current Tree로 적용하는 과정을 거친다.
- 이러한 React의 렌더링 방식을 통해 브라우저에서 일어나는 리플로우나 리페인팅에 따른 렌더링 비용을 최소화할 수 있게 된다. (렌더링이 빨라지는 것은 아니자, 주의하자!)
🔖 참고 자료
The rendering process of a web page.
React 톺아보기 - 05. Reconciler_5 | Deep Dive Magic Code
React 톺아보기 - 05. Reconciler_2 | Deep Dive Magic Code
To Understand React Fiber, You Need to Know About Threads
React Fiber & Concurrency. Part 1
⚛ React Hooks: Lifecycle Diagram
https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/
Server Component 알아보기 (2) - 렌더링 과정과 사용 시의 이점?
🔄 서버 컴포넌트의 렌더링 과정
지난 시간에서 서버 컴포넌트의 탄생 과정을 살펴봤다면, 이번 시간에는 본격적으로 서버 컴포넌트가 어떤 식으로 렌더링되어 사이트에 반영되는지를 살펴보는 시간을 가지도록 하겠다.
(부족하거나 틀린 내용에 대한 의견은 언제든지 감사히 받겠습니다.)
- 서버가 요청을 받고 브라우저에 보낼 응답값 만들기 시작
우선 서버에서는 브라우저에 렌더링시킬 문서를 만들 작업을 진행한다.
여기서 React의 root 컴포넌트부터 만들게 되는데, 서버에서 렌더링을 진행해야 하므로 사실상 root 컴포넌트부터가 서버 컴포넌트로 생성이 된다.
server-components-demo/server/api.server.js at main · reactjs/server-components-demo
- 서버가 루트 컴포넌트 엘리멘트를 JSON으로 직렬화
이후, root 컴포넌트는 현재 요청받은 페이지에서 HTML 태그들을 포함한 컴포넌트들을 직렬화하기 시작한다.
직렬화? (Serialization)
특정 데이터나 객체 등을 다른 컴퓨터 환경에 저장하고 이를 다시 꺼내와 재구성하거나 재활용할 수 있는 포맷 등으로 변환 혹은 복원하는 과정을 가리킨다.
이해가 잘 안 갈 수 있으니 아래의 예시 코드를 보자.
// 아래의 Test 컴포넌트를 만들었다고 하자. function Test() { return <div>배고파</div>; } // 해당 코드는 React에서 createElement를 통해 생성이 된다. React.createElement(Test, { children: "배고파" }); // 그 코드를 언제든지 재구성할 수 있도록 json의 형태로 변환한다. { $$typeof: Symbol(react.element), type: Test props: { children: "배고파" }, ... }
위와 같이 서버에서는 루트 컴포넌트부터 차례대로 해당 페이지의 컴포넌트들을 json화(JSON.stringify())하는, 직렬화하는 과정을 거치게 된다.
다만, 위의 과정은 서버 컴포넌트일 경우에만 해당되며 클라이언트 컴포넌트는 좀 다른 형태로서 json화되어 들어간다.
"use client" function ClientComp (result:boolean) { return (<div>하이고</div>) } { $$typeof: Symbol(react.element), type: { // 아래와 같이 'module.reference'를 쓴다. 이는 클라이언트 컴포넌트에서 쓰인다. $$typeof: Symbol(react.module.reference), // name에는 우리가 흔히 쓰는 export default를 가리킨다. name: "default", // filename에는 클라이언트 컴포넌트의 파일 경로를 나타낸다. filename: "./src/ClientComp.js" }, props: { children: "하이고" }, }
왜 이런 과정을 거치게 되는 걸까?
이전 글에서 우리는 서버 컴포넌트의 내용에 대해서 아래와 같은 내용을 이야기했다.
- 서버에서 단 한 번만 렌더링이 이루어진다.
- 클라이언트에서 사용되는 JavaScript 코드가 없다.
- 이 말은 즉, 사이드 이펙트나 동작 등이 필요한 코드를 적용할 수 없으므로 React Hook 또한 사용할 수 없다.
위와 같은 내용을 기반으로 보면 서버 컴포넌트에서는 순수하게 요소 만을 담은 컴포넌트만을 담게 될 것이다.
클라이언트 컴포넌트는 동작을 담당하는 코드인 만큼, 안에 요소 외에도 다양한 핸들링 함수들이 존재하게 될 것이며, 그 중에는 분명 외부의 요소를 가져와서 내부 함수에서 받아 동작하도록 만든 클로저로 구성된 함수들도 있을 것이다.
이럴 경우 해당 컴포넌트의 범위에서만 만들어져야 하는데, 외부 변수 등을 포함해야 하는 상황에서 범위를 벗어나게 되는 상황이 발생하다보니 직렬화하기가 어려워진다.
이런 이유로 클라이언트 컴포넌트에서는 컴포넌트의 직렬화를 하기 보단, 해당 위치에 어떠한 컴포넌트가 배치될 것인지를 알려주기 위해 ‘placeholder’같은 역할을 하는 module reference 객체가 생성된다.
이러한 과정은 ‘react-server-dom-webpack'와 같은 번들러 등을 통해서 적용된다.
서버 컴포넌트와 클라이언트 컴포넌트를 배치할 때 참고해야 할 사항이 두 가지가 있다.
하나는 서버 컴포넌트에 props로 클라이언트 컴포넌트를 담을 경우 위와 같은 이유로 직렬화 구조가 망가질 수 있다는 점이다.
그리고 다른 하나는 , 클라이언트 컴포넌트에서는 서버 컴포넌트를 직접 return할 수 없다는 점이다.
위에서 이야기했듯 클라이언트 컴포넌트는 module reference가 먼저 배치되는 식이다.
그런데, 이 컴포넌트에서 return으로 서버 컴포넌트를 배치하면 어떻게 될까?

이 경우, 바운더리가 클라이언트 컴포넌트에 귀속되어 처리된다.
그래서 클라이언트 컴포넌트 속에 서버 컴포넌트를 배치해야 할 때, 클라이언트 컴포넌트에 귀속되지 않게 하려면 클라이언트 컴포넌트에서 해당 컴포넌트를 {children}으로 받아 내려주도록 해야한다.
이렇게 root에서부터 마지막 자식 컴포넌트까지 서버 컴포넌트와 클라이언트 컴포넌트를 json으로 만드는 걸 그림으로 보면 아래와 같다.

- 브라우저에 렌더링된 내용을 전달
브라우저는 이제 서버로부터 직렬화된 JSON을 받고 이를 브라우저에 반영되도록 렌더 트리를 구축하고 배치한다.
즉, 아까 전에 직렬화한 내용을 역직렬화(JSON.parse())하게 된다.
이 과정 자체도 번들러를 통해서 진행하게 되는데, 구축 중에 module reference인 객체를 발견하게 된다면 이제 여기에 배치할 클라이언트 컴포넌트가 무엇인지 확인하고 이 객체를 컴포넌트로 바꾼 후 계속해서 다음 과정을 진행하게 된다.

이러한 과정을 거치고 난 뒤, 트리 배치가 완료되면 DOM에 커밋하는 것으로 렌더링이 끝난다.
👍 서버 컴포넌트의 이점
이러한 서버 컴포넌트의 과정을 통해 어떠한 이점을 우리는 볼 수 있을까?
hydrate할 컴포넌트량의 축소
SSR 방식에서는 컴포넌트 요소들을 다 배치한 다음에 hydration을 통해 컴포넌트들에 이벤트 핸들러 등을 배치하게 된다.
하지만, 이 과정 속에서 상호작용이 필요한 컴포넌트의 children으로 받아지는 UI만 컴포넌트들도, 해당 컴포넌트와 얽혀있어 같이 hydrate되어 드러나게 된다.
서버 컴포넌트를 이용하면 위에서 이야기했듯 이런 UI만 그려지는 컴포넌트들은 json으로 담아져서 html 문서에 바로 반영되며, 핸들링 등이 동작하는 컴포넌트들과 엮어지지 않으므로 hydrate할 컴포넌트의 양이 줄어든다.
컴포넌트별로 가능한 데이터 페칭
SSR 방식에서는 Next.js 기준으로
getServerProps()와 같은 방식은 각 페이지에서 서버 데이터를 받아서 데이터를 내려주는 식으로 적용된다.그러다보니 데이터를 리페칭할 경우, 이를 다시 반영해야 하는 만큼 페이지 전체에 렌더링이 적용되는 부하가 발생한다.
서버 컴포넌트의 방식은 이처럼 페이지가 아닌 각 서버 컴포넌트에서 데이터 페칭을 하고 이를 반영해주기 때문에 렌더링에 관한 부하를 줄일 수 있다.
줄어드는 JavaScript 번들 크기
서버 컴포넌트는 json화되어 문서에 바로 반영되어 서버가 브라우저에 보내준다. 이는 이제 클라이언트 컴포넌트에 필요한 JavaScript 데이터만을 다운받게 함으로서 웹 페이지가 받게되는 번들의 양을 줄여준다.
서버 컴포넌트는 실질적으로 사용된 지 얼마 되지 않아, 아직도 많은 논의가 오가고 있다.
하지만 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 활용하면 프로젝트를 좀 더 가볍고, 적절하게 개선하는 데에 도움이 될 수 있는 만큼 적극적으로 사용을 시도해보자.
🔖 참고 자료
Where do React Server Components fit in the history of web development?
Making Sense of React Server Components

