방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2024년 여름 회고

😱 세상에 벌써 9월이라니

뭐 한 것도 없는데 내일부터 9월이라고 한다.
뭔가 이룬 것도, 한 것도 없는데 30대 초의 여름은 너무나도 순식간에 지나간 것 같다.

음.. 아니다, 정정하겠다. 뭔가... 뭔가... 많았던 것 같다.

개인 프로젝트를 열심히 만들기도 했고, 서류를 열심히 지원 넣기도 했고, 면접도 보고, 공부도 하고, 병원도 다니고.. 한 것은 참 많았다.
다만, 결실을 맺은 것이 하나도 없었던 것 같다.

왜 이렇게 많은 일이 있었고, 힘들었는데 벌써 여름이 사라지는가

오랜만에 회고를 쓰고 싶어서 써봤는데 다 쓰고 보니 찡찡거리는 내용 밖에 없더라...ㅋㅋㅋㅋ...

그럼에도 쓰는 것은 올 여름은 정말 정신적으로도, 육체적으로도 힘들었다.
그래서 글로나마 속 안의 응어리를 찡찡대는 것으로 덜어내고자 써보기로 했다.


🥶 날씨는 더운데, 서류 결과는 차갑다

여름은 내내 서류를 넣고, 회사를 면접다니는 시간의 연속이었다.

잡코리아에서 서류를 넣고, 없으면 사람인에서 서류를 넣고, 없으면 원티드에서 서류를 넣고, 없으면 잡플래닛에서 서류를 넣고, 없으면 점핏에서 서류를 넣고, 없으면 인쿠르트에서 서류를 넣고, 없으면 다시 잡코리아로… 도르마무의 연속이었다.

이 글을 작성할 때에 한 달에 얼마나 서류를 넣었는지 확인해보니 한 달 평균 50개는 넣고 있었던 듯 하다.

면접은 요 여름 동안 10회 내외로 있었는데, 결과는 모두 불합격이었다😇

힘들어도 다시 일어나는 자가 일류랬다. 흡.

쓴 고배를 계속 마시게 되니, 점점 여러 생각이 들기 시작했다.

내가 지식이 부족한 게 문제인 걸까, 혹은 면접 태도가 문제였을까.
아니면 정말 운이 좋지 않은 거였을까, 시기가 좋지 않았던 걸까..

‘그래도, 면접까지 간 것이 어디냐’와 ‘이렇게 또 면접 경험 또 쌓았다.’라는 생각으로 면접 탈락의 고배를 마실 때 스스로를 안심시켰다.

아니, 이렇게라도 생각해야 했다.

취업이 절실한 현 상황에 이렇게라도 생각하지 않으면 진짜 무너질 거 같았다.


😵 너무나도 힘들었던 6월과 7월, 그리고 다시 보게 된 ‘야구’

전체적으로 무너지기 시작한 시기는 무더위와 습함이 점점 커지기 시작한 6월 중순부터였다.

실업급여가 끝난 시점부터 어찌됐든 돈을 다시 벌어야 하는데, 임시로 아르바이트를 하기엔 언젠가 취업이 될 것이라는 근거 없는 믿음에 섣불리 하질 못했다.

거기에 직전 회사의 일로 인해 다시 해야지라는 생각이 돌아오지 못했다.

받지 못하는 200만 원이 넘는 월급은 아직도 감감무소식이었고, 그 짧은 한 달 간의 노력은 물거품이 되었으며, 고소를 지급하기엔 모두가 무력감을 느끼기도 했고, 간이대지급금은 회사 설립일로 인해 받지도 못해서 정말 아무런 수확도 거두지 못한 처지가 됐다.

그 충격 때문인지 뭔가를 하는 것도 쉽지 않았고, 갑자기 생각나면 가슴이 답답하고 홧병이 터져서 견디기가 어려웠다.

그래서 한동안은 침대에서, 방에서 나가지 못하고 처박혀있기만 했던 것 같다.

얼마 전에 받은 체납통보. 회사를 잊으려고 하면 이런 식으로 뭔가 하나씩 기억나게 만든다.

거기에 알게 모르게 들린 나를 향한 외부 사람들의 불쾌한 이야기와 집에서도 밖에서도 신경쓰이는 부분들이 발생하니까 진짜 견디기가 너무 힘들었다.

그래서 이런저런 일들이 겹치다 보니 멘탈 케어를 위해서라도 잠시 동안 휴식이 필요하겠다 싶었고, 모든 SNS를 다 지우고 멘탈 회복에 집중하기 위한 시간을 가지기로 했다.


그렇게 아무 것도 안하고 진짜 연락도 멀리하고 보낸 한 달은 완전 회복은 아니더라도 어느 정도의 디톡스 효과를 좀 봤다.

다만 그 기간 중에 많은 것을 지워버리니 주변이 조용해져서, 부정적인 생각으로부터 생각을 돌릴 것이 필요했는데... 그 때 우연히 ‘최강야구’를 보게 됐다.

이 프로그램을 보면서 조금씩 기운을 내기 시작했다.

‘최강야구’라는 프로그램은 여러 가지 이유로 은퇴하거나, 또는 나와 같이 취업을.. 즉 프로로 가기 위한 선수들이 야구를 하고 싶어서 모이고, 그래서 여러 고등, 대학, 독립 리그 팀들과 야구 팀으로서 경기를 하는 프로그램이다.

우연히 봤던 프로그램은 등장했던 한 사람, 한 사람의 절실함과 노력에 몰입하며 보게 됐고, 많이 울면서 방송을 통해 어딘가에서의 위로감을 느꼈다.

너무나도 어렵고 힘들고 각박한 현실에라도, 열심히 하는 사람들이 보여주는 그 모습이 아무래도 많은 위로가 되었었나 보다.

그렇게 우연히 보던 프로그램은 매주 월요일마다 찾아보는 프로그램이 되었고, 자연스럽게 이 프로그램을 보다 보니, 다시금 예전에 좋아했던 ‘야구’를 직접 보고 싶다는 생각이 들었다.

그래서 어릴 때부터 응원했던 팀인 ‘LG 트윈스’ 경기를 찾아보기 시작했는데, 요즘도 야구를 하는 날이면 LG 경기를 라디오처럼 옆에다 틀어놓고 귀로 들으면서 작업하고 있다.


여담이지만 LG 선수들 내에서는 각별히 좋아하는 선수가 한 명 있는데, 바로 불펜 수호신 김진성 선수이다.

NC에 있던 이 선수가 방출이 되면서 모든 구단에 연락을 돌렸다가 LG에 연락을 했을 때 차명석 단장이 이 선수를 ‘김진성이니까 와라’라고 하면서 데려갔다는 이야기는 야구팬들 사이에서는 워낙 유명한 이야기이다.

그리고 이렇게 LG 트윈스에 들어간 노익장은 불펜의 수호신이 되어서 23-24 시즌의 1이닝을 책임지는 사람이 되었다.

힘들었던 시기를 견디게 해주고 기운을 내게 만들어 준 용기를 준 번호, 42.

김진성 선수는 인터뷰를 할 때 항상 하는 말이 ‘매 순간, 매 상황이 마지막이라고 생각하고 최선을 다한다’고 한다.

은퇴를 생각할 수 있는 나이임에도 노력해서 야구가 하고 싶어 자신을 어필하고, 어렵게 들어간 구단에서 자신의 모든 것을 쏟아부어 매 경기 세이브를 이뤄내는 이 선수의 모습에 너무나도 감명을 받고 기운을 얻었다.

조만간 야구 직관을 가고 선수 옷도 구입하고 싶은데, 등번호를 새긴다면 첫 번째는 무조건 빛진성으로 해야겠다고 생각했다.^^

이렇게 야구는 어느 새인가 오랜만에 내 일상에 스며들어왔고, 물론 요즘 나에게 분노를 주는 존재지만... 그래도 야구 덕분에 우울감에서 해방될 수 있었다.

그런 의미에서 LG 트윈스가 올해도 우승했음 좋겠다. (안 될 것 같지만… 쓰읍…)

🤒 아프다, 아프다, 또 아프다.

이렇게 멘탈이 괜찮아졌을 즈음에, 다시금 본격적인 취업 준비를 하기 위해 구글 스타트업 캠퍼스를 오고가면서 개인 프로젝트 작업과 이력서 지원을 지속적으로 하고 있던 와중, 어느 날 잠을 자려고 하는데 등골이 오싹해지는 느낌이 들었다.

딱 봐도 몸살 기운이라는 게 느껴졌고 약을 먹고 누웠으나 이게 감기로 인한 몸살이 아니라는 걸 알기까지 오랜 시간이 걸리지 않았다.

장염이 와버렸다. 그것도 엄청 심해서 아무 것도 먹지도 못하고 아무 것도 하지 못 할 정도의 장염에 걸려서 1주일을 내내 침대에서 보냈다.

이 시기에 정말 서러웠는데, 위에서 말한 어려웠던 시기에서 이제 겨우 회복해서 좀 다시 시작하려고 노력했더니 아무 것도 못 할 정도로 장염에 걸려 무기력하게 되어버리니 뭘 할 엄두가 나지 않았다.

진짜 일주일 내내 나는 왜 이래야 하나 별별 생각도 들고 서러워서 며칠은 내내 울었던 거 같다.

진짜 아프다아아아아아아학

그렇게 아픔을 딛고 지낸 6월 말과 7월 초를 지나 이제 당분간 안 아프겠지 생각한 현재의 8월...
염병할 코로나가 내 몸을 한 번 거쳐갔다.ㅋㅋㅋㅋ.....

다만, 8월에는 멘탈 상태가 괜찮아서 그런가 ‘어, 아프네?’라고 생각만 했고, 대수롭게 여긴 걸 보니 새삼스럽지만 ‘멘탈 상태’의 중요성을 깨닫기도 했다.

💪 그리고 다시 돌아온 트위터, 진전이 나간 개인 프로젝트

그래서 위에서 저렇게 이야기하고 요 여름에 그래서 한 게 없느냐 물으면 그렇진 않았다.

먼저 개인프로젝트 이야기를 좀 해보자. 위의 일들이 있고 나서, 오랜만에 본 개인 프로젝트의 코드는 그야말로 끔찍하다는 느낌이 가득했고 전체적으로 엎어야겠다는 결심을 했다.

그래서 Vite와 React 기반의 프로젝트를 Next.js App Router 기반의 프로젝트로 바꿨는가 하면, 한편으로는 여태까지 짜놨던 코드 방식들이 다 마음에 안 든다고 코드를 리펙토링하고 그러다보니 아예 새로 만든 프로젝트처럼 되어버렸다.

진짜... 뭔가... 뭔가 많이 함.

사실상의 리빌딩을 통해 깨달은 건 내가 TypeScript를 되게 지협적으로 쓰고 있었으며, 전역 상태 관리도 그냥 썼지 왜 써야하고 무엇이 좋은지는 생각해보지 않았어서 이번 리빌딩을 통해 그런 부분들을 좀 깨달을 수 있는 기회가 됐다.

그리고 다시금 스스로 지향하는 ‘사이드 프로젝트 주도 개발’에서 추구하는 것이 무엇인지도 깨달았다.


현재 그 개인 프로젝트는 몇 가지 기능을 추가하는 것으로 테스트 런칭까지 올 수준이 왔다.

올해 안에 본격 런칭이 목표이므로 조만간 트위터 등으로 공개하는 일이 있을 것 같다.


아, 트위터 이야기가 나와서 말인데 위에서 말한대로 모든 것을 삭제하고 야구를 보며 지내다 좀 괜찮아져서 돌아오려고 했더니… 한 달이 지나 있었다, Oh…

R.I.P 나의 옛 계정이여. 2~3년 동안 지랄 같은 일들도 많았지만 좋은 사람도 많이 만났다.

그래서 결국 새 계정을 팠는데, 오히려 처음으로 돌아와서 그런가 마음이 편했다.

보고 싶은 거만 보고, 하고 싶은 말만 하는 계정이 되기도 했고…

사실상 개발 계정이 아니게 된 것 같으니 그냥 이쪽을 추구하기로 했다. 오히려 좋아^^

✅ 마지막 근황, 사이드 프로젝트와 단기직 근무 시작

아참, 그래서 이렇게 줄줄 이야기했지만 결국 마지막 이야기를 안 했는데..

우선 또 다른 사이드 프로젝트를 진행하기로 했다. 작년 너굴콘을 통해서 연사자로 연이 닿게 된 한날 님을 통해 토이 프로젝트 모임에 참가하게 됐다.

시작한 지 얼마 되지 않았는데, 얼마 전까지는 사실 이걸 진행하면서 느꼈던 고민으로 인해 어떻게 할지 망설이다가 며칠 전 한날 님에게 이야기를 드렸는데 깊은 말씀을 해주셔서 반성하고 다시금 집중해보려고 한다.

프로젝트가 잘 될 지는 모르겠지만, 적어도 프로젝트를 하시는 분들께 누를 끼치지 않기 위해 노력해야겠다. (하지만, 어제 실수를 일으켜버렸지만,,,🤦)


그리고 며칠 전에 모 회사에 단기계약직 프론트엔드 개발자로서 지원하게 됐는데, 라이브 코딩에서 사실상 망쳤기 때문에 떨어졌구나 해서 신경쓰지 않고 있었다.

근데 단기계약직으로 일해보지 않겠냐고 제의가 들어왔고, 당장의 돈도 없는 데다 일 경험이 너무나도 중요하다고 생각해서 제의에 승낙해 며칠 전부터 단기계약직으로 일하게 됐다.

오랜만에 이 시간대에 보는 야경이 너무나도 감사함을 느끼는 요즘.

현재 하는 일은 백오피스 근무를 하고 있는데, 이런저런 잡음이 있긴 하지만 첫 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 클라우드 서비스이다.
보통 앱을 배포할 때에는 항상 AndroidiOS 모두 심사를 거쳐서 업데이트 내용을 배포해야 하는데, 이 경우 시간이 오래 소요된다.

하지만 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 패키지 설치와 그 연동 작업을 진행해보기로 하겠다.


참고 자료

learn.microsoft.com

velog.io

React에서의 렌더링 과정, 그리고 Fiber

📄 머리말

React를 공부해 본 사람들이라면 가상 DOM(Virtual DOM)이라는 이름을 들어본 사람은 적지 않을 것이다. 가상 DOM을 통해 우리가 React에서 수정한 내용들이 담겨지고 이를 실제 DOM에 반영한다는 건 나도 그렇고, 많은 사람들이 아는 이야기인데.. 하지만, 정작 그 과정에 대해 알아보려고 하진 않았던 것 같다.

그래서 이번 글에서는 기존 DOM에서의 렌더링부터, 가상 DOM, 그리고 이 가상 DOM을 이용한 React에서의 렌더링에 대해 공부한 내용을 정리해봤다. 이번에 공부한 내용을 모던 리액트 Deep Dive 책을 참고해서 공부했다.

부족하거나 잘못 정리한 내용이 있다면 언제나 댓글로 감사히 받고 참고하겠다.

📶 브라우저에서의 렌더링 과정에 대해서 잠시 짚고 가보자.

우선 DOM에 대해서 한 번 짚고 가자.

DOM은 Document Object Model의 약자이며, 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.


브라우저에서는 이 DOM을 이용해서 화면을 구상하고, 배치하고, 그려낸 뒤 이용자에게 보여준다.

  1. 브라우저가 사이트의 HTML 파일을 다운로드 받는다. 파싱 단계
  2. 브라우저에서는 HTML을 파싱하고, 각 노드로 구성된 트리를 만든다. (DOM) DOM 트리 구축 단계
  3. 파싱 중 CSS 파일을 발견하면 해당 파일을 다운로드하고, 이 역시 CSS 노드로 구성된 트리를 만든다. (CSSOM) CSSOM 트리 구축 단계
  4. 파싱이 완료되면 완성된 DOM 트리를 최상위인 html부터 최하위 노드까지 순회하여 화면에 보여지지 않는 요소들(dispaly:none)은 배제하고 그려질 노드들만 렌더 트리로 구축한다. 렌더 트리 구축 단계
  5. 이후 구축이 완료된 렌더 트리를 기준으로 똑같이 html부터 순차적으로 CSS 스타일 요소를 적용하기 시작한다. 렌더 트리 배치 단계
    1. 과정은 화면에 어디에 배치할지를 계산하는 레이아웃부터 진행된다.
    2. 이후, 레이아웃이 끝나면 색을 입히는 과정인 페인팅이 진행된다.
  6. 이 과정까지 완료 되면 최종 출력물인 웹 페이지가 브라우저에 노출된다. 렌더링 단계

해당 과정을 도식화하면 아래와 같이 보여질 수 있다.

출저: https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/
출저: https://medium.com/@gneutzling/the-rendering-process-of-a-web-page-78e05a6749dc

이후 화면에 그려진 다음 상호작용 과정 등이 발생하면 리플로우나 리페인팅이 발생하게 된다.

  • 리플로우(Reflow)
    • 특정 레이아웃 등의 변화로 인해서 배치가 변경되거나 크기가 수정됐을 경우 레이아웃 계산을 다시 하게 되는데 이 과정을 리플로우라고 가리킨다.
    • 레이아웃이 변화하게 되는 만큼, 리플로우 이후에는 리페인팅도 다시 일어난다.
  • 리페인팅(Repainting)
    • 위와 같이 리플로우가 일어나거나, 또는 특정 상황에 의해서 요소의 색상이 변경되거나 할 경우에 화면에 다시 색을 입히거나 변경해주는 과정을 진행하게 되는데 이를 리페인팅이라고 부른다.

브라우저는 위와 같은 과정을 거쳐서 웹 페이지를 렌더링하게 된다.

이해가 되지 않는다면 요소가 간단히 있는 페이지에서 네트워크를 느리게 하고 다운 받는 파일들을 확인해보면 html을 먼저 받고, css 파일을 그 다음에 받는 것을 확인할 수 있다.

처음에 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의 과정을 살펴보는 글을 쓰게 되면 다뤄보도록 하겠다.
    • 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 파이버 아키텍처 분석

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, index
    • child는 해당 Fiber가 가지고 있는 하위 Fiber를 가리킨다.
      • 여기서 한 가지 알고 있어야 할 것은 React의 모든 컴포넌트나 DOM 노드 등은 하나의 Fiber 객체를 부여받으며 이 Fiber는 가장 먼저, 첫 번째 Fiber를 가리킨다.
    • sibling은 자식 Fiber나 같은 depth의 Fiber와 관련하여 다음에 올 Fiber를 가리킨다.
    • return은 이런 자식 Fiber를 담고 있는 부모 fiber를 가리킨다.
    • index 은 같은 depth에 있는 Fiber들 중에서 어떤 순서를 가지고 있는지를 나타낸다.

출저: https://medium.com/@jettycloud/react-fiber-concurrency-part-1-b9d287077d1b

  • pendingProps, memoizedProps
    • 위에서 살펴봤던 props가 변경됐을 때 리렌더링이 일어난다고 했는데, 그 props를 관리하는 내용이 이 두 속성이다.
    • pendingProps는 전달받은 props가 렌더링이 일어나기 전에 배치되는 내용이다.
    • memoizedProps는 렌더링이 완료된 후 pendingProps가 옮겨질 내용이다. 렌더링 이후 pendingProps가 여기로 옮겨진다고 생각하면 된다.
  • updateQueue
    • Fiber와 관련하여 필요한 작업들을 담아두는 Queue이다.
      • 상태를 업데이트 한다던가, 콜백 함수를 실행시킨다던가 등의 작업들이 관리된다.
  • memoizedState
    • 현재의 함수형 컴포넌트와 관련하여 Hook Function들이 여기에 저장된다.
  • alternative
    • 변경 전후의 Fiber를 비교하기 위해 필요한 값이다.

내용 중에 이야기를 했지만 1개의 Fiber에는 1개의 컴포넌트나 DOM 노드 등과 엮어져 있으며, 그리고 이런 Fiber는 state가 변경되는 등의 DOM의 변경이 필요한 상황에서 실행된다. 이런 Fiber는 keystateNode, tag 등을 통해 보이듯이 UI를 단순하거나 복잡한 값들을 이용해서 객체에 관리하고, 작성한 JavaScript의 코드에 맞게 표현되도록 적용되어 있다.

이런 Fiber를 통해 React가 표현하고자 하는 것은 크게 세 가지이다.

  • 각 컴포넌트나 DOM 노드 들을 1개의 Fiber로 단위를 쪼갠 뒤, indexchild, 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에서는 이 두 트리를 이용해서 각각 변화된 사항을 처리하고 있다.

출저: https://medium.com/@ejtac/react-fiber-algorithm-28b7a7665081

이 과정을 이용해서 React는 업데이트가 발생하면 workInProgrss Tree에 새로운 데이터를 반영하고 빌드하기 시작한다.

그리고 빌드가 완료되면 다음 렌더링에 이 트리를 사용해 렌더링을 반영하게 되고, 그렇게 되면 workInProgress Tree의 내용이 이제 current Tree로 덮어씌워지게 된다.

좀 더 쉬운 이해를 위해 이미지를 보도록 해보자, 아래는 current Tree이다.

출저: https://dev.to/afairlie/to-understand-react-fiber-you-need-to-know-about-threads-3dof

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

출저: https://dev.to/afairlie/to-understand-react-fiber-you-need-to-know-about-threads-3dof

Fiber의 작업 순서는 크게 파이버 작업을 수행하는 beginWork(), 작업이 완료되면 파이버 작업을 끝내는 completeWork(), 그리고 모든 Fiber의 변경점이 반영됐을 시에 수행되는 commitWork()로 이루어진다.

위의 그림에서는 다음과 같은 과정으로 진행될 것이다.

  1. <a1>에서 beginWork() active
  2. <b1>에서 beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  3. <b2>에서 beginWork() active, 자식이 있으므로 <c1>으로 이동
  4. <c1>beginWork() active, 자식이 있으므로 <d1>으로 이동
  5. <d1>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  6. <d2>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  7. <c1>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  8. <b2>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  9. <b3>에서 beginWork() active, 자식이 있으므로 <c2>로 이동
  10. <c2>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  11. <b3>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  12. <a1>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  13. 최상위인 <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가 어느 부분에서 일어나는지 확인해보자.

출저: https://medium.com/@galmargalit/react-function-components-hooks-lifecycle-diagram-14f76e0a5988

💮 정리

지금까지 언급된 과정들을 요약해서 정리해면 다음과 같다.

  • 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의 렌더링 방식을 통해 브라우저에서 일어나는 리플로우나 리페인팅에 따른 렌더링 비용을 최소화할 수 있게 된다. (렌더링이 빨라지는 것은 아니자, 주의하자!)

    🔖 참고 자료

wikibook.co.kr

The rendering process of a web page.

React 파이버 아키텍처 분석

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 Algorithm

React Fiber & Concurrency. Part 1

⚛ React Hooks: Lifecycle Diagram

https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/

Server Component 알아보기 (2) - 렌더링 과정과 사용 시의 이점?

🔄 서버 컴포넌트의 렌더링 과정

지난 시간에서 서버 컴포넌트의 탄생 과정을 살펴봤다면, 이번 시간에는 본격적으로 서버 컴포넌트가 어떤 식으로 렌더링되어 사이트에 반영되는지를 살펴보는 시간을 가지도록 하겠다.

(부족하거나 틀린 내용에 대한 의견은 언제든지 감사히 받겠습니다.)

  1. 서버가 요청을 받고 브라우저에 보낼 응답값 만들기 시작

우선 서버에서는 브라우저에 렌더링시킬 문서를 만들 작업을 진행한다.

여기서 React의 root 컴포넌트부터 만들게 되는데, 서버에서 렌더링을 진행해야 하므로 사실상 root 컴포넌트부터가 서버 컴포넌트로 생성이 된다.

server-components-demo/server/api.server.js at main · reactjs/server-components-demo

  1. 서버가 루트 컴포넌트 엘리멘트를 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으로 서버 컴포넌트를 배치하면 어떻게 될까?

출저: https://www.joshwcomeau.com/react/server-components/

이 경우, 바운더리가 클라이언트 컴포넌트에 귀속되어 처리된다.

그래서 클라이언트 컴포넌트 속에 서버 컴포넌트를 배치해야 할 때, 클라이언트 컴포넌트에 귀속되지 않게 하려면 클라이언트 컴포넌트에서 해당 컴포넌트를 {children}으로 받아 내려주도록 해야한다.

이렇게 root에서부터 마지막 자식 컴포넌트까지 서버 컴포넌트와 클라이언트 컴포넌트를 json으로 만드는 걸 그림으로 보면 아래와 같다.

출저: https://www.plasmic.app/blog/how-react-server-components-work

  1. 브라우저에 렌더링된 내용을 전달

브라우저는 이제 서버로부터 직렬화된 JSON을 받고 이를 브라우저에 반영되도록 렌더 트리를 구축하고 배치한다.

즉, 아까 전에 직렬화한 내용을 역직렬화(JSON.parse())하게 된다.

이 과정 자체도 번들러를 통해서 진행하게 되는데, 구축 중에 module reference인 객체를 발견하게 된다면 이제 여기에 배치할 클라이언트 컴포넌트가 무엇인지 확인하고 이 객체를 컴포넌트로 바꾼 후 계속해서 다음 과정을 진행하게 된다.

출저: https://www.plasmic.app/blog/how-react-server-components-work

이러한 과정을 거치고 난 뒤, 트리 배치가 완료되면 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

리액트 서버 컴포넌트의 동작 방식

Next) 서버 컴포넌트(React Server Component)에 대한 고찰

How React server components work: an in-depth guide

Server Component 알아보기 (1) - CSR, SSR, 그리고 서버 컴포넌트?

😤 면접 때 받은 질문, ‘서버 컴포넌트가 어떤 식으로 렌더링되는지 아시나요?’

어제(22일) 기준으로 현재까지 면접 본 곳 중에서 가장 좋다고 생각되는 곳에서 면접을 보고 왔다.

역시 이번에도 내 지식의 밑천이 너무 쉽게 드러났고, 그로 인해서 집에 돌아와서 이불을 덮고 눈물을 찔찔 짰다, 아이고, 자신감을 왜케 잃어버렸나, 내 자신!

그렇게 이불킥 좀 하고 정신차린 뒤, 면접에서 나왔던 질문들을 되짚어보던 중에 기억에 남던 질문이 하나 있었다.

서버 컴포넌트가 어떤 식으로 렌더링이 되는지 아시나요?

사실 Next.js를 선택했던 이유는 SEO와 초기 렌더링 개선을 위해서였다.

이를 위해 서버 컴포넌트를 통해 데이터를 받아와서 반영한다고만 생각했지, 이 데이터를 어떻게 반영하는지도 모를 뿐더러 애초에 서버 컴포넌트를 쓰는 방법이 뭔지, 더 나아가 서버 컴포넌트가 왜 생겼고, 무엇을 위해서 존재하는지 생각해본 적이 없다.

그냥 서버 컴포넌트라는 게 있으니까 쓰는거지!! (잘못된 생각)

덕분에 면접 때 이런 질문들에 격침을 당할 수 밖에 없었고, 이번 기회에 Next.js에서 제대로 쓰기 위해서 서버 컴포넌트에 대해서 좀 더 알아야겠다 싶어 정리해보기로 했다.

그 첫 내용으로 서버 컴포넌트가 생겨나기까지 과정을 적어봤으며, 아래 글의 내용을 최대한 가져와 이해한 내용을 정리해봤다.

Where do React Server Components fit in the history of web development?

(부족하거나 틀린 내용에 관한 의견은 언제든지 감사하게 받겠습니다.)

해당 글은 React를 기준으로 웹 페이지 개발 변천사에 대한 대략적인 이야기를 다루고 있다. 다른 프레임워크나 라이브러리에 대한 이야기는 하지 않으므로 참고하길 바란다.

👵 할아버지, 할머니 어렸을 적 들려주신 정적 웹 페이지 시대

서버 컴포넌트를 이야기하기 위해서는 React를 통해서 구현되는 SPA, 단일 웹페이지 애플리케이션(Single Page Application)가 무엇이고 왜 등장했는지, 그리고 그 한계가 무엇인지를 짚고 가야한다.

예전의 웹 페이지는 지금처럼 동적이지 않고 HTML과 CSS를 이용한 정적인 웹페이지만을 담당했다.

브라우저가 GET 요청을 보내면 웹 서버는 HTML 파일을 반환, 브라우저는 이를 파싱하고 렌더 트리를 구축하고, 배치하여 화면에 렌더링시키는 게 일반적이었다.

🕹️ JavaScript와 함께 동적 웹 페이지의 시대가 개막

시간이 지나면서 사람들의 요구 사항은 당연히 늘어나고, 자연스럽게 이 웹 페이지에서 특정 동작 등을 처리할 수 있기를 바라게 된다.

상호작용의 요구에 따라 HTML과 CSS에 이어 웹 페이지의 동작을 담당하는 JavaScript가 탄생하고 웹 페이지에 적용하게 되었다.

이 때까지만 해도 JavaScript에서의 동작은 Form 유효성 검사, Hover 등의 이미지 변경 등과 같은 페이지 상호작용이나 애니메이션 처리 등에만 쓰였다.

데이터 처리 등과 같은 동작은 여전히 서버에서 다 담당하게 됐다.

🚢 Ajax, 그리고 SPA

그리고 JavaScript의 적용으로 복잡한 웹 페이지를 만드는 게 가능해진 개발자들은 이윽고, Ajax(Asynchronous Javascript And Xml, ‘비동기식 자바스크립트와 xml’) 방식을 이용해 JavaScript와 서버 간의 직접적인 상호작용을 시도하게 된다.

이 방식으로 서버에서는 더 이상 html 파일을 반환할 필요 없이 데이터 만을 반환해서 넘겨주고, JavaScript에서는 받아온 데이터를 이용해 html 문서에 내용을 그려준다.

이 방법은 점점 사용성이 더더욱 커지면서 웹 생계에 엄청난 영향을 줬고, 이윽고 웹 어플리케이션, 즉 브라우저에서 이용 가능한 웹 프로그램이 쏟아지게 된다.

그리고 이러한 생태계에서 등장하게 된 어플리케이션 방식이 바로 SPA, 단일 페이지 어플리케이션이다.

  • 클라이언트에서는 Ajax의 등장으로 데이터만을 서버에 받아와 반영할 수 있게 되면서, 필요할 때마다 HTML을 새로 받을 필요성이 없어졌다.

    필요에 따라 변화가 필요한 부분만 새로 반영해주는 식으로, 렌더링을 최소화하거나 페이지를 전환할 필요성이 떨어지게 된다.

  • 반면 서버 쪽에서는 그동안 html 문서 파일을 항상 보내줬으나, 더 이상 그러지 않아도 되므로 좀 더 다양한 방식의 데이터 방식들이나 통신 규칙들이 등장하게 된다.

    XML의 양식에서 벗어나 JSON이 등장하고, REST API 등이 등장한 것엔 이러한 영향이 있다.

SPA의 등장으로 웹의 생태계는 엄청 확장되었고 다양한 변화를 불러일으키게 됐으며, 다양한 라이브러리와 프레임워크가 쏟아지게 됐다.

Angular, Vue, 그리고 React 등이 생겨나기 시작한 건 이 때부며, 백엔드와 프론트엔드 사이의 경계가 생기기 시작한 것 또한 그러하다.

😖 SPA에서 발생하는 문제점들, 그리고 SSR 방식으로의 회귀

그러나 SPA 방식에서의 웹 어플리케이션의 구현은 몇 가지 문제점을 야기하게 된다.

  • 서버 측에서 데이터를 받아서 화면에 적용되기 전까지, 어플리케이션 화면에는 아무것도 나타나지 않는다.

    이는 검색 엔진에서 화면을 크롤링할 때 첫 화면에 아무것도 나오지 않아 색인 등을 하지 못해 검색 결과를 망가뜨릴 수 있는 중요한 원인이 된다.

  • SPA의 ‘대다수’는 CSR, Client Side Rendering 방식을 채용하고 있다. (SPA ≠ CSR이다!)

    CSR의 방식은 다음과 같이 진행된다.

    1. 서버에서 HTML 파일을 가져온다.
    2. 웹 페이지에서 작동할 JavaScript를 가져와 파싱 후, 코드를 실행한다.
    3. 웹 페이지는 서버에 데이터를 요청하고 응답을 기다린다.
    4. 응답을 받으면 수신된 데이터를 기반으로 웹 페이지를 렌더링한다.

      Client Side Rendering 과정을 설명한 이미지

이러한 방식에서는 SPA의 사이즈가 클 수록 JavaScript와 데이터를 받아오는 시간에 따라 오랜 로딩 시간을 가지게 되는데, 이용자들 입장에서는 불편함을 초래할 수 밖에 없게 된다.

이로 인해 고민에 빠진 개발자들은 이 방식을 해결하기 위해 이전까지 쓰였던 방식이었던 SSR, Server Side Rendering에 눈을 돌리기 시작했다.

↩️ 원본 회귀? Server Side Rendering으로 눈을 돌린 사람들

SSR 방식은 CSR 방식 이전까지 웹 페이지들에서 다 쓰이던 방식이었다.

작동 방식은 다음과 같다.

  1. 서버에서 HTML 파일을 가져온다.
  2. 사이트에서는 가져온 HTML 파일을 바로 화면에 렌더링하고(이 때의 상태를 Ready to Render 라고 부르는 듯하다.) JavaScript 파일을 가져오기 시작한다.
  3. JavaScript 파일을 다운 받는 동안에는 화면의 동작이 전부 정지된다. 단 이용자가 어떤 동작을 행하려고 하는지 그 입력 사항은 브라우저 측에서 기억해둔다.
  4. JavaScript 파일들이 받아지면 이를 통해 받아온 데이터는 화면에 노출되기 시작할 것이며, 그 사이에 기억해둔 유저의 동작이 있다면 해당 동작을 실행해준다.

    Server Side Rendering 과정을 설명한 이미지

React는 CSR 방식을 채택한 라이브러리이며 createRoot()라는 함수를 이용해 id=”root”가 있는 HTML 요소를 체크하고 해당 요소를 기준으로 React의 코드를 반영하고 있다.

그래서 아까도 말했지만 CSR 방식으로는 처음 렌더링 되는 텅 빈 html에 React로 만든 컴포넌트들이 그려지거나 핸들링 코드들이 적용되는 식으로 웹 페이지에 반영된다.

이런 React의 코드를 SSR 방식으로 쓰기 위해, React에서는 ReactDOMServer라는 객체를 Node 서버에서 사용할 수 있도록 추가했다.

ReactDOMServer – React

이 객체를 이용해서, 서버에서는 react의 root 컴포넌트가 렌더된 html 파일을 보내주고 브라우저는 이를 받아 html 내용을 화면에 출력한다.

// server/index.tsx
import express from 'express';
import fs from 'fs';
import path from 'path';
import ReactDOMServer from 'react-dom/server';
import App from '../client/App';

const app = express();
// 클라이언트 사이드에서 빌드된 html을 읽어와 사용
const html = fs.readFileSync(path.resolve(__dirname, "../client/index.html"), "utf-8");

app.get("/", (req, res) => {
  // <App /> 을 렌더링
  const renderString = ReactDOMServer.renderToString(<App />);
  // <div id="root"></div> 내부에 삽입
  res.send(html.replace('<div id="root"></div>', `<div id="root">${renderString}</div>`));
});
// 위의 / 이외의 경로로 요청할 경우(js, css 등)
// dist/client 폴더에 있는 파일들 제공
app.use("/", express.static("dist/client"));

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

자, 그렇다면 기존의 CSR 방식에서는 html 파일을 가져오고, JavaScript 파일을 가져와 코드를 배치하고 실행까지 시켜야 화면에 렌더링이 된다고 이야기했다.

그러나 SSR 방식에서 root 컴포넌트는 서버에서 이미 렌더링해서 화면에 띄운 상태가 되는데, React의 컴포넌트나 핸들링된 코드들을 어떤 식으로 배치해서 화면에 그려주고 동작시켜주는 걸까?

바로 Hydration, 수분 공급 또는 수화라는 방식이다.

이전 글에 정리한 적이 있었는데, 지금 돌이켜보니 이번 기회 덕분에 그 의미를 더 정확하게 알게된 것 같다.

SSR에선 html 파일을 먼저 가져와 화면에 그리고(Ready-to-render), 그 후 번들링된 JavaScript 파일을 가져와 배치한다고 이야기했다.

React에서는 이렇게 컴포넌트나 핸들링 코드들을 받아온 뒤, 콸콸콸 쏟아붓는다라는 의미에서 hydrateRoot()를 이용해서 root 컴포넌트에 그 코드들을 쏟아붓듯이 배치한다.

ReactDOMClient – React

조금 더 자세하게 이야기하자면, React에서 SSR 방식을 위해서 JavaScript와 데이터를 받은 뒤 페이지에 렌더링하기 전에, 현 페이지의 가상 DOM과 서버에서 생성된 DOM을 비교한다.

그 후, React에서는 비교가 완료된 DOM에 React와 상호작용을 가능하도록 하는 이벤트 핸들러를 붙이게함으로서 HTML에 React와 관련한 컴포넌트나 동작들을 가능하게 해준다.

이렇게 정적 HTML에 React 코드들을 콸콸 채워간다는 의미로 이 과정을 Hydration이라고 부르는 듯 하다.

아무튼 Hydration 방식의 등장으로 많은 프론트엔드 개발자들이 React를 SSR 방식으로 사용하기 위한 시도들이 곳곳에서 보이기 시작했다.

하지만, 그럼에도 이 방식을 이용하는 데에는 몇 가지 문제가 있었다.

🤔 하지만 이러한 SSR 방식에도 문제가 있었으니…

이런 식으로 SSR을 쓰기 위한 개발자들의 연구에도 불구하고 몇 가지 문제점을 맞이하게 된다.

  • Hydration으로 인한 렌더링 시간 증가

    아무래도 HTML 파일을 미리 받아와서 화면을 그려내는 초기 렌더링 속도는 빨라질 수 있겠지만, 문제는 향후 React를 이용해서 그려내고자 하는 컴포넌트 내용들은 Hydration을 과정을 거쳐야 하기 때문에 DOM을 비교하고, 그 사항을 반영하기까지 시간이 걸릴 수 밖에 없다.

    이 부분은 SSR 방식을 채용한 사람들에게 고민을 안기는 이슈 중 하나가 됐다.

  • 불필요한 번들 사이즈의 증가

    사실상 SSR 방식으로 HTML이나 JavaScript 코드들을 받아온다고 하면, React에서 컴포넌트를 그리기 보다는 HTML 파일 등에 요소를 배치하는 게 더 이득일 것이다.

    어쨌든 SSR 방식으로 필요에 의해서 React에 컴포넌트를 그려내기는 한다지만, 이는 결과적으로 불필요한 번들 사이즈를 늘릴 수 밖에 없는 문제를 초래하게 된다.

이외에도 다른 이슈들이 있겠지만, 아무튼 React를 만든 사람들은 이런 부분에 머리를 맞대면서 고민을 하기 시작한다.

그리고, 그 해결책으로서 꺼낸 것이 서버 컴포넌트라는 카드였다.

💻 그리고 서버 컴포넌트의 등장

어찌됐던 Hydration의 영향으로 발생할 수 있는 문제들이 고민하던 React 측에서는 Hydration을 불필요할 때에 쓰지 않도록 하기 위한 서버 컴포넌트를 만들게 된다.

그리고 컴포넌트를 서버용, 클라이언트용으로 두 개를 쪼갠 뒤 그 역할을 각각 맡기려고 시도한다.

  • 서버 컴포넌트
    • 서버에서 단 한 번만 렌더링이 이루어진다.
    • 클라이언트에서 사용되는 JavaScript 코드가 없다.
      • 이 말은 즉, 사이드 이펙트나 동작 등이 필요한 코드를 적용할 수 없으므로 React Hook 또한 사용할 수 없다.
  • 클라이언트 컴포넌트
    • 여러 번 렌더링이 일어날 수 있다.
    • 클라이언트 내에서 상호작용 하는 JavaScript 코드들이 배치되어 있다.

여기서 알아둬야 할 점은 서버 컴포넌트의 방식은 SSR, 그러니까 서버 사이드 렌더링과 다르다.

  • 서버 컴포넌트의 코드들은 클라이언트로 전달되지 않는다. 엄밀히 말하자면 html 문서에 포함되어 보내진다고 보면 된다.

    하지만, SSR은 html에 미리 렌더링될 root와 같은 요소만 담아질 뿐이지, 동작하는 모든 코드들은 사실상 JavaScript에 번들링되어 보내진다.

  • 서버 컴포넌트에서는 각 컴포넌트에서 fetch 등을 통해서 데이터에 접근하는 게 가능하다. 하지만 SSR은 Next.js 기준으로 최상위 페이지에서만 getServerProps()과 같은 값을 통해서 서버와 통신하여 데이터에 접근하게 된다.

  • SSR의 경우에는 데이터가 갱신되어 리페칭하게 된다면 위에서 이야기했듯 최상위 페이지에서 데이터가 갱신되기 때문에 전체 페이지가 갱신되어 클라이언트 상태를 유지하는 게 힘들다.

    서버 컴포넌트는 이와는 다르게 특정한 형태로 받아지게 되어 있어, 데이터가 갱신되어 리페칭되어도 영향을 받지 않는다. 클라이언트 상태를 그대로 유지할 수 있다는 장점이 있다.

    서버 컴포넌트는 이런 형태의 데이터를 참조해 반영된다. / 출저: https://maxleiter.com/blog/creeperhost-api

여기까지 서버 컴포넌트가 탄생하게 된 배경을 적어봤다.

다음 글에서는 그래서 서버 컴포넌트는 어떤 식으로 렌더링이 되는지, 그리고 이를 가볍게 적용한 사례와 이점에 대해서 작성해보도록 하겠다.

🔖 참고 문서

Where do React Server Components fit in the history of web development?

ReactDOMServer – React

ReactDOMClient – React

[SSR] 서버사이드렌더링(2) - SSR 직접구현, ReactDOMServer

React SSR (서버 사이드 렌더링) 얕게 시작해보기 (React.hydrateRoot)

가장 쉬운 방법으로 리액트에서 서버사이드 렌더링 이해하기

새로 등장한 ‘리액트 서버 컴포넌트’ 이해하기 | 요즘IT

React Server Components – React

React 18: 리액트 서버 컴포넌트 준비하기 | 카카오페이 기술 블로그

리액트 서버 컴포넌트의 동작 방식

2024-07-12: Lighthouse 등을 이용한 지표 분석과 메인 페이지 최적화 시도

🤯 마이그레이션을 했는데 성능이…

본래 SSR로 마이그레이션하면서 개인적으로 CSR 때보다 기대하고 있던 거라면 당연, 초기 페이지 로딩 속도였다.

그러나, 마이그레이션을 마친 뒤 로그인 후 메인 페이지에 진입하는 걸 지켜봤는데 생각과 달리 느리다는 느낌을 많이 받게 됐다.

첫 Lighthouse 성능 지표. 이때까지만 해도 CSR보다 지표가 더 떨어졌다.

Route (app)                              Size     First Load JS
┌ ○ /                                    1.58 kB         226 kB
├ ○ /_not-found                          0 B                0 B
├ ○ /element/create                      142 B          80.8 kB
├ ○ /main                                3.56 kB         222 kB
├ ○ /redirect                            4.8 kB          214 kB
├ ○ /search                              3.39 kB         222 kB
├ ○ /travel/create                       4.27 kB         217 kB
├ ○ /user/forget                         2.25 kB         215 kB
├ ○ /user/forget/send                    178 B          87.8 kB
├ ○ /user/login                          2.48 kB         223 kB
├ ○ /user/reset                          2.33 kB         216 kB
├ ○ /user/reset/completed                178 B          87.8 kB
├ ○ /user/signup                         2.48 kB         216 kB
├ ○ /user/signup/completed               1.55 kB        82.2 kB
└ ○ /user/verified                       3.06 kB        92.2 kB
+ First Load JS shared by all            80.7 kB
  ├ chunks/864-5566f80c48d657ca.js       27.6 kB
  ├ chunks/fd9d1056-acda956f51940821.js  51.1 kB
  ├ chunks/main-app-df49185495db17a6.js  230 B
  └ chunks/webpack-07331cd7f4a99096.js   1.79 kB

후에 이 원인을 분석하고 메인 페이지를 고쳐본 결과 만족할만한 성능을 이끌어내게 되었는데, 이번 글은 그 과정에 대해서 이야기해보고 또 이를 분석하기 위해 이용했던 도구들 이야기를 좀 해보려고 한다.

🔦 웹 페이지의 품질 지표를 나타내는 Lighthouse

웹 페이지를 최적화하기 위해서는 우선 이걸 분석할 수 있는 도구가 필요했다.

크로니움 기반의 웹 브라우저들은 개발자 도구에 기본적으로 웹 페이지의 성능을 책정해주는 도구가 탑재되어 있는데, 이게 바로 Lighthouse이다.

Lighthouse를 이용하면 웹 페이지의 성능 뿐만 아니라 접근성이나 SEO 등의 사항 등을 진단하고 지표로 보여준다.

뿐만 아니라 이런 웹 페이지의 지표에서 문제 사항과 개선 방향성을 제시해준다.

이처럼 지표 아래에 진단 사항을 보여주면서 개선 방향성을 제시해준다.

Lighthouse에서 측정하는 지표는 크게 네 가지(+PWA) 이다.

  • 성능
    • 웹 페이지의 초기 페이지 로딩 속도를 확인한다.
  • 접근성
    • 웹 페이지가 얼마나 사용자 측면에서의 웹 접근성을 지키고 있는지를 보여준다.
  • 권장사항
    • 웹 페이지가 얼마나 웹 표준 및 보안 사항을 지키고 있는지를 보여준다.
  • 검색엔진 최적화
    • 웹 페이지가 얼마나 검색 결과에 잘 표시되거나, 크롤링이 가능한지를 보여준다.
  • PWA*
    • PWA로 만든 앱이라면 추가로 측정되는 지표다. 서비스 워커나 오프라인 동작 등이 잘 동작하는지를 측정하며, 일반 웹 페이지는 무관하다.

이 중 웹 개발을 하는 사람들은 성능 탭을 많이 보는데, 성능은 또 다섯 개의 지표를 분리해서 본다. 기존(Lighthouse 8)에는 6개였으나 현재(Lighthouse 10)는 5개로 변경되었다.

  • FCP (First Contentful Paint)
    • 사용자가 페이지를 처음 탐색한 시점부터 페이지 콘텐츠의 일부가 화면에 렌더링된 시점까지의 시간을 측정한다.
  • SI (Speed Index)
    • 페이지 로드 중 콘텐츠가 시각적으로 표시되는 속도를 측정한다.
  • LCP (Largest Contentful Paint)
    • 사용자가 처음 페이지로 이동한 시점을 기준으로 표시 영역에 표시되는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간을 보고한다.
  • TBT (Total Blocking Time)
    • FCP 후 입력 응답을 방지하기에 충분한 시간 동안 기본 스레드가 차단된 총 시간을 측정한다.
  • CLS (Cumulative Layout Shift)
    • 페이지의 전체 수명 주기 동안 발생하는 모든 예상치 못한 레이아웃 변경에 관한 레이아웃 변경 점수의 가장 큰 버스트를 측정한다.

현재 내 웹 페이지에서는 FCP, LCP 그리고 SI에서 지표가 두드러지고 있다.

특히 LCP는 주황색으로 주의를 띄고 있으므로 이번엔 요 세 가지를 줄여보는 노력을 해보려고 한다.

📊 번들의 크기를 분석해주는 Bundle Anaylzer

우선 무작정 최적화를 하기 보다는 진단 항목에서 알려준 대로 어떤 부분이 문제인지를 파악해보기로 했다.

그 중에 ‘자바스크립트 줄이기’와 ‘사용하지 않는 자바스크립트 줄이기’ 항목에 있는 파일을 열어봤는데 이 중에 내가 짠 코드와 관련없는, Firebase의 코드만이 존재했다.

항목에 있던 파일들을 열어봤더니…

전부 firebase의 firestore와 관련한 내용들이었다.

그 밖에도 ‘렌더링 차단 리소스 제거하기’엔 css 파일이 있었는데, 이건 Tailwind CSS의 class들이었다.

문제가 되는 css파일을 열어보면…

Tailwind에서 생성한 css 파일이다.

분명 Tailwind는 빌드할 때, 사용하지 않는 class들은 제외하고 css를 생성한다고 했다. 혹시나해서 파일 내에 사용하지 않는 class들을 검색해봤는데 그건 확실한 듯 한 걸 보니 그렇게 빌드가 됐음에도 파일이 무거운 거 같았다.

이쯤되면 빌드 시, 얼마나 번들 파일이 무거운지 궁금해지기 시작해졌는데 마침 리본(@ribbon_with_u) 님께서 bundle analyzer가 있으니 한 번 써볼 것을 권장해서 설치하고 써봤다.

Next.js에서는 아래의 패키지를 설치하면 된다.

yarn add @next/bundle-analyzer

이후 빌드 시 분석하기 위해서 next.config.mjs 파일에 다음과 같이 설정해주자.

import createBundleAnalyzer from '@next/bundle-analyzer';

// openAnalyzer를 true로 하면 빌드 후, 자동으로 분석 페이지가 열린다.
const bundleAnalyzer = createBundleAnalyzer({
  enabled: true,
  openAnalyzer: true,
})(nextConfig);

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  distDir: "./dist",
};

export default bundleAnalyzer(nextConfig);

설정 후 이제 프로젝트의 빌드를 시도하면 번들링을 분석하는 페이지들이 열린다.

  • client.html
    • 클라이언트에서 필요한 모듈과 페이지 등의 번들링한 결과를 보여준다.
  • edge.html
    • 서버에서 필요한 모듈 등의 번들링한 결과를 보여준다.
  • nodejs.html
    • 서버 사이드 렌더링 시에 필요한 모듈들을 번등링한 결과를 보여준다.

현재 프로젝트에서는 client.html과 nodejs.html만 지표가 출력됐는데 그 결과는 아래와 같다.

nodejs.html
client.html

뚜렷하게 보이는 단어는 ‘firebase’였다. 두 페이지 모두 그 규모가 크게 나타나는 것을 보니 번들이 상당히 크구나 싶었다.

Tailwind와 관련한 css 파일은 보이지 않았던 걸 보니 그리 큰 편은 아닌 것 같다.

원인도 파악했으니 일단 해볼 수 있는 만큼 성능 최적화를 진행해보고자 한다. 그 과정을 지금부터 서술하겠다.

💪 그래서 시도해봤다, 최적화.

  • 로더 컴포넌트를 메인에 덮어씌우자

    이 방법은 사실 해결했다기 보더는 눈속임에 가깝다. LCP 지표에서 아래와 같은 진단 결과가 나왔다.

해당 컴포넌트를 확인해보니 현재의 코드는 유저 정보를 받아올 때까지 로더가 나오고, 받아와지면 메인 페이지 컴포넌트를 ‘교체’하는 식이었다.

export default function Layout({ children }: { children: ReactNode }) {
  const user = useContext(AuthContext);

  if (!user) {
    return (
      <Backdrop colorTheme="loader">
        <Bar />
      </Backdrop>
    );
  }

  return (
    <>
      <Header activeSearch={true} useSideBar={true} />
      <Suspense>
        <MainHeader username={user?.displayName} />
      </Suspense>
      <section className="flex flex-col w-full justify-center items-center">
        {children}
      </section>
    </>
  );
}

그러나 교체되는 방식은 오히려 성능 면에서 좋지 않은 느낌이었고, 그래서 로더를 ‘교체’가 아니라 메인 컴포넌트 위에 뚜껑처럼 ‘덮어씌우기’는 방식으로 코드를 변경해봤다.

export default function Layout({ children }: { children: ReactNode }) {
  const user = useContext(AuthContext);

  return (
    <>
      {!user && (
        <Backdrop colorTheme="loader">
          <Bar />
        </Backdrop>
      )}
      <Header activeSearch={true} useSideBar={true} />
      <Suspense>
        <MainHeader username={user?.displayName} />
      </Suspense>
      <section className="flex flex-col w-full justify-center items-center">
        {children}
      </section>
    </>
  );
}

이후 재측정한 결과는 놀라웠는데, FCP는 0.2초로 확 줄고, LCP도 1.0초로 줄었다.

  • main에서 useContext 호출 위치를 한 곳으로 모으고 props drilling로 처리해보자

    메인 페이지에서는 유저의 계정 정보를 받는 AuthContext가 layout과 list-section에 적용되고 있다.

    그러나 차라리 AuthContext를 page에서 한 번만 받아오고, props drilling으로 그 값을 내려주면 좀 더 렌더링이나 페이지 최적화에 영향을 주지 않을까 싶어 시도해보기로 했다.

      export default function Main() {
        const user = useContext(AuthContext);
    
        return (
          <>
            {!user && (
              <Backdrop colorTheme="loader">
                <Bar />
              </Backdrop>
            )}
            <MainHeader username={user?.displayName} />
            <ListSection uid={user?.uid} />
          </>
        );
      }
    
      export default function ListSection({ uid }: { uid?: string }) {
        const { list, router } = useListSection(uid);
          .
          .
          .
        );
      }
    
      export default function useListSection(uid?: string) {
        useEffect(() => {
          if (!uid) {
            return;
          }
          .
          .
          .
        }, [uid]);
      }
    

    이후, 빌드를 시도해봤는데 우선 용량에서 조금이나마 줄어드는 효과가 있었다.

      // 빌드 전
      Route (app)                              Size     First Load JS
      ├ ○ /main                                3.56 kB         222 kB
    
      // 빌드 후
      Route (app)                              Size     First Load JS
      ├ ○ /main                                1.54 kB         202 kB
    

    다만, 측정 결과에 관한 지표는 그대로였다.

  • Firestore의 용량을 줄이기 위해 동적으로 적용해보자

    Firebase 중에서 가장 큰 용량을 차지하는 건 Firestore였다.

    그러나 라이브러리의 번들을 줄이는 방법은 너무나도 생소하게 느껴져서 이를 어떻게 해야할까 생각하고 있었는데, 아까 전의 리본 님께서 이런 링크를 가져와주셨다.

    How to reduce firebase bundle size?

StackOverFlow에 나온 내용에 따르면, Firestore를 동적으로 필요할 때만 가져오도록 처리했다.

이런 식으로도 받아올 수 있을 줄이야! 그래서 즉각 아래와 같이 코드를 수정해봤다.

// 변경 전
export const firestore = getFirestore(firebase);

// 변경 후
**export async function firestore() {
  const { getFirestore } = await import("firebase/firestore");
  return getFirestore(firebase);
}**

그리고, 이후 API 로직에서 firebase를 받아오는 코드를 아래와 같이 수정했다.

class TravelService {
  static async getUserTravelList(userUid: string, keyword?: string | null) {
    try {
      let travelList: TravelBasicInfoType[] = [];
      const docsState = await getDocs(
        collection(**await firestore()**, `travels`, userUid, "docs")
      );
      .
      .
      .
    }
  }
}

덧붙여서 page에서는 Firestore에서 동적으로 데이터를 받아올 것을 고려해서 ListSection 컴포넌트를 Lazy 컴포넌트로 묶었다.

const ListSection = lazy(() => import("./_components/list-section"));

export default function Main() {
  const user = useContext(AuthContext);

  return (
    <>
      {!user && (
        <Backdrop colorTheme="loader">
          <Bar />
        </Backdrop>
      )}
      <MainHeader username={user?.displayName} />
      <ListSection uid={user?.uid} />
    </>
  );
}

과연 이렇게 하면 유의미한 결과를 가져올 수 있을까?

// 빌드 전
Route (app)                              Size     First Load JS
├ ○ /main                                1.54 kB         202 kB

// 빌드 후
Route (app)                              Size     First Load JS
├ ○ /main                                1.08 kB         122 kB

빌드를 시도한 결과 main 페이지의 용량이 급격하게 줄어들었다!

측정 결과도 체크해보니 SI나 LCP도 줄어들었다!

😖 하지만 부딪친 한계점과 최적화 후기

이렇게 최적화를 진행해서 지표가 좋게 나왔지만, 진행되지 못 한 부분도 있었다.

  • Tailwind CSS의 번들 크기를 줄여보자, 아래 문서를 읽고 시도해봤지만 번들링에서 생성된 코드가 최대였다(…)

    Optimizing for Production - Tailwind CSS

  • Firebase의 번들 크기를 더 줄여볼 수 있을까 싶어 시도해봤으나 이 역시 실패했다.

아울러 SI의 경우에는 빠르면 0.6초, 느리면 1.3초까지 들쑥날쑥하는 결과를 보이기도 했다. 아마 Firebase에서 데이터를 받아오는 시간차에 따라서 이 영향이 좀 생기는 것 같은데.. 그래도 몇 번 시도를 해보니까 평균적으로는 1.0초를 유지하고 있다.

최적화 전의 성능 점수는 93~94점에서 머물고 있다면, 최적화 후의 성능 점수는 97~99점 사이를 유지하고 있다.

최초의 Lighthouse 분석 결과

그리고 최적화를 진행 한 후의 Lighthouse 분석 결과

이렇게 지표를 보고 최적화를 해본 경험은 처음이었는데, 유의미한 성과를 만들어 낸 것 같아 기쁘다.

향후 프로젝트가 런칭이 되면, 지표를 확인해보면서 어떤 부분이 문제였는지 파악하고 좀 더 이용자들이 쾌적하게 이용할 수 있는 사이트를 만들기 위해 노력해봐야겠다.

🔖 참고 자료

Lighthouse Chrome 확장 프로그램에서 검색엔진 최적화 감사 카테고리 출시  |  Google 검색 센터 블로그  |  Google for Developers

Optimizing: Bundle Analyzer

First Contentful Paint (FCP)  |  Articles  |  web.dev

속도 색인  |  Lighthouse  |  Chrome for Developers

Largest Contentful Paint (LCP)  |  Articles  |  web.dev

Total Blocking Time (TBT)  |  Articles  |  web.dev

Cumulative Layout Shift (CLS)  |  Articles  |  web.dev

How to reduce firebase bundle size?

Optimizing for Production - Tailwind CSS