mailgithub

나의 첫 번째 오픈소스 기여

포기하지 않고 끝까지 파고들기

thumbnail

처음으로 오픈소스에 이슈를 등록하고, PR 을 올려 기여하게 되었습니다.
문제 정의, 디버깅을 통해 문제 범위를 좁혀가고, 해결 방안을 찾아가는 과정을 기록하려 합니다. 🎉

오픈소스 기여?

"오픈소스" 여러분들은 이 단어를 보면 어떤 생각이 드시나요?
'많은 시간이 들어가는 것', '기여해보고 싶다', '실력이 좋아야 할 수 있는거 아닐까?' 와 같은
생각이 들 것 같습니다. 저도 똑같이 생각하고 있었거든요.

이렇게 생각하고 있던 제가 Swiper 라는 슬라이더 오픈소스에 기여하게된 과정을 공유하고자 합니다. ⭐️

누락된 타입

회사 내 사이드 프로젝트를 진행하던 어느날..
열심히 슬라이드 기능을 구현하기 위해 라이브러리를 찾고 있었습니다.
라이브러리 도입을 위해 React 지원, 필요한 기능 지원, 많은 사용 사례, 유지 보수 여부 등을 살펴보았고,
최종적으로 Swiper 라는 라이브러리를 선택하게 되었습니다.

열심히 기능 구현을 하던 와중 타입 에러를 마주하게 되었습니다.

first error

[첫 번째 에러] 문제 정의

null 타입 지원이 안된다는 에러였으나,
공식 문서에서는 분명 초기값을 null 로 할당하는 것으로 쓰여있습니다.
"내가 뭔가 잘못 생각한건가?", "공식 문서가 잘못된거 아닌가?", "관련해서 이슈는 없나?" 등
많은 생각과 함께 천천히 이슈를 파악하고, 분석을 진행했으며,
분석 진행 결과 "이건 수정이 필요한 것이 맞다!" 였습니다.

[첫 번째 에러] 이슈 제보

이슈를 등록하기 전까지 수도 없이 검증, 또 검증하였고,
저의 첫 오픈소스 이슈 등록을 하게 되었습니다. 🎉
(당시 찍어둔 사진이 없어 글 작성할 시점에 찍어둔 사진으로 대체합니다.)

first error issue

[첫 번째 에러] 문제 해결

얼마 지나지 않아, 메인테이너 분께서 null 타입 추가하는 커밋을 남겨주시면서
첫 번째 에러 해결과 함께 무사히 저의 이슈는 닫히게 되었습니다. 🎉

하지만 null 타입 지원 이슈 말고 다른 문제도 있었으니..
그건 바로 개발 환경에서 실행조차 못하게 하는 치명적인 에러였습니다.

cannot read properties of undefined

second error

타입스크립트를 쓴 이 후로 되게 오랜만에 본 에러였습니다ㅠㅠ...
첫 번째 에러인 타입 에러와 함께 계속 보인 에러 화면이며,
이상하게 Swiper Controller 를 사용하면 에러가 발생하고 있었습니다.

첫 번째 에러와 마찬가지로 문제 정의 부터 시작했습니다.

[두 번째 에러] 문제 정의

Next js 에서 Swiper 라이브러리의 controller 기능을 사용할 경우 런타임 에러가 발생한다.

버그 발생 조건이 워낙 간단하여 issue 로 등록된 것이 없나 주의깊게 살펴보았고 역시나
controller 외 다른 기능에서도 비슷한 에러가 많았으며, controller 역시 동일 이슈가 있었습니다!

동일 이슈가 있다니 천만 다행입니다.. 그리고 해결책도 있었습니다!!
해당 issue 에서 적힌 해결책은 controller 초기값에 null 이 아닌 빈 객체를 넣으라고 했습니다.

first solution

빈 객체를 넣어보니? 정말 잘 동작했습니다. "와 문제 해결~ 🎉" 이라고 마무리 지었을 수도 있지만
빈 객체로 인한 타입 에러도 발생하고 있었고, 스스로도 찝찝하다는 생각이 자꾸만 들었습니다.

👍 따봉 인증 마크도 5개, 이슈 제보자 분도 도움이 되었다고 했고
메인테이너도 해당 이슈를 확인 후 이슈 closed 를 했단 긍정적인 지표들이 보이고 있지만
이게 과연 최선의 해결책일까? 라는 의문이 계속 들었습니다.

우선 일정을 위해 빈 객체 할당 후 프로젝트는 계속 진행 했으며,
프로젝트와 별개로 퇴근을 하고 난 후 내가 정확히 어떤 의문점을 가지고 있는지
차근차근 작성하기 시작하며 문제 해결을 위한 긴 여정을 시작했습니다.

제가 가지고 있는 의문점은 아래와 같습니다.

  1. controller 의 타입에는 객체가 포함되어 있지 않다.
    그렇기 때문에 typescript 사용 시 type 에러가 발생한다.
  2. 값이 없음을 명확히 표현하기 위해서 초기값은 null 이 맞는 것 같다.
    즉 빈 객체를 초기값으로 할당하는 것은 좋은 방향이 아닌 것 같다.
  3. React 17 에서 null 을 할당했을 때 정상 동작을 했다는 것은
    React 18 에서도 null 을 할당했을 때 정상 동작 해야한다는 의미로 해석할 수 있다.

3번의 내용처럼 React 17 에서는 controller 초기값에 null 을 할당하더라도
문제의 에러가 발생하지 않았으며 그렇기에 지금의 해결책에 대해 더욱 의문이 생겼습니다.
의문과 동시에 문제를 더 좁은 범위로 생각하여 하나의 가설을 세울 수 있었습니다.

React 17, React 18 두 버전 사이에서 어떤 동작 차이로 인해 Swiper controller 동작에 이슈를 발생시켰다.

현재의 에러를 비교 확인하실 수 있는 테스트 환경입니다!
직접 에러를 보시면서 읽어보시면 좋을 것 같아요! 🤗

[두 번째 에러] 문제 좁히기

React 17 에서 18 로 넘어오면서 어떤 변경점이 있었는지 확인하기 시작했고,
React 18 의 새로운 기능을 정리한 docs 를 토대로 확인했습니다.
Concurrent, Suspense, Server Component 등 다양한 추가, 변경사항이 있었고
그 중에서 눈에 띄는 건 변경된 Strict Mode 동작 방식이였습니다.

요약하자면 아래와 같습니다.

앞으로는 React 가 상태를 유지하면서 UI 섹션을 추가하고 제거할 수 있는 기능을 추가하고 싶다.
구성 요소가 여러 번 마운트되고 제거되는 효과는 resilient 해야한다.
React 18 에서는 이 과정에서 발생할 수 있는 문제를 쉽게 도출하기 위한 새로운 strict mode 검사를 도입한다.
구성 요소가 처음으로 마운트될 때마다 자동으로 모든 구성 요소를
unmount 후 다시 mount 하여 이전 상태를 복원한다.

마침 디버깅하기 위해 확인한 controller value 에서도 unmount 의 흔적을 찾아볼 수 있었습니다.

destroyed controller

Swiper 소스 코드를 보면 useIsomorphicLayoutEffect hook (자체 custom hook) 에서
정리 함수로 destroy 관련 함수를 실행하는 것을 확인할 수 있었으며
이는 React 18 의 변경된 strict mode 동작 방식에 의해 실행 되었다는 의미로 해석할 수 있습니다.

그렇다면 여기서 한가지 궁금증이 생깁니다.

StrictMode 를 사용하지 않는다면 지금의 문제는 발생하지 않는 것일까?

궁금하니 바로 실행으로 옮기겠습니다! next.config.js 에서 reactStrictMode 를 false 로 변경해보았습니다.
결과는? 예상처럼 이슈는 발생하지 않았으며
Swiper 객체가 정상적으로 할당된 것을 확인할 수 있었습니다.

strict mode false

이를 기점으로 가설은 곧 확신이 되었습니다.

React 18 에서 변경된 strict mode 동작 방식이 Swiper controller 동작에 이슈를 발생시켰다.
React 17, React 18 에서의 어떠한 동작 차이 === strict mode 동작 방식 차이

[두 번째 에러] 디버깅

이제 어느 부분이 의도와 다르게 동작되고 있는지 살펴보면 될 것 같습니다.
에러 파악을 위한 힌트는 에러 화면에서 잘 보여주고 있습니다.
위에서 본 에러 화면을 다시 볼까요?

second error

지금은 잘 모르겠지만 updateSwiper 실행하고 이것저것 실행하다가
마지막 Swiper.maxTranslate 함수를 실행할 때 어떤 값이 undefined 인 상태여서 발생한 에러로 보입니다.
천천히 위 에러 메시지를 기반으로 node modules 내 Swiper 라이브러리를 파헤치기 시작했습니다.
동시에 정확한 원인 파악을 위해 마지막 실행 함수의 그 이전 setControlledTranslate 함수가 실행될 때
관련 값들을 파악할 목적으로 debugger 를 사용했습니다.
(디버거 사용 방식은 이 링크를 통해 참고하시면 됩니다!)

그리고 이런 에러가 발생하게된 원인을 확인할 수 있었습니다!

conditional debugger

[두 번째 에러] 에러 원인 파악

setControlledTranslate 내의 c.maxTranslate() 를 실행할 때 c 는 { destroyed: true } 상태입니다.
{ destroyed: true } 상태이지만 Swiper 객체이므로 maxTranslate 함수를 트래킹하여 소스 코드를 호출할 수 있습니다.

error destroyed

maxTranslate 함수를 실행하게 되면 this.snapGrid 배열에서 length 로 접근하게 됩니다.
이 때 this 는 c 즉, { destroyed: true } 상태의 Swiper 객체를 의미합니다.
Swiper 객체에선 snapGrid 의 값을 확인할 수 없어 undefined 로 추론되고
이 상태에서 length 로 접근해서 cannot read properties of undefined 에러가 발생하게 되었습니다.

error function

지금까지 파악된 내용을 기반으로 아래와 같이 정리할 수 있을 것 같습니다.

React 18 의 변경된 strict mode 동작 방식으로 인해
mount -> unmount -> mount 의 동작이 이루어졌고,
unmount 과정에서 destroyed 된 Swiper 객체를
controller 에서 사용하여 지금의 에러가 발생했다.

[두 번째 에러] 에러 해결

destroyed 된 상태의 Swiper 객체에 접근하는 것을 막아주면 위 문제를 해결할 수 있을 것 같습니다.
이왕이면 swiper 객체에 대한 validation 로직이 있는 곳에서 함께 처리하면 더 좋을 것 같아
controller 로직 내에서 적절한 위치를 찾아 적용했습니다!

conditional

swiper.controller.setTranslate 함수를 실행하면 setControlledTranslate 가 실행되게 됩니다.
해당 함수는 Swiper 객체가 destroyed 된 상태에서 돌아가면 의미가 없고, 에러를 발생시키는 함수이므로
setTranslate 함수 실행 전 early return 조건문에 추가하여 문제를 해결할 수 있었습니다.

[두 번째 에러] 이슈 제보 및 PR

문제 해결책과 함께 신나는 마음으로 이슈 등록하는 과정에서 동일한 이슈와 이미 해결이 되어있는 것을 확인했습니다. 😰
되게 아쉬웠으나 해당 이슈에서의 해결책을 보면서 문제 해결은 했지만 조건문 위치를 수정하는 방향으로
재수정이 가능할 것 같다는 생각이 들었습니다. 저는 해결을 할 때 하나의 조건을 더 고려했습니다.

Swiper 객체에 대한 validation 로직이 있는 곳에서 함께 처리하자

해당 이슈에서의 처리 방식은 문제를 야기하는 setControlledTranslate 함수 내에서 destroyed 여부를
확인 후 early return 하고 있으며, 그 외 불필요한 로직을 실행하는 것을 확인할 수 있었습니다.

다시 기쁜 마음으로 최대한 정중하게 이슈를 등록 했으며
위 이슈에 대한 언급과 함께 동일한 문제를 해결한다는 점을 명시했습니다.

second error issue

그리고 해당 이슈에 대한 PR 도 올리게 되었습니다!
얼마 지나지 않아 메인테이너께서 해당 PR 을 확인 머지를 해주셨고,
저의 첫 오픈소스 PR 및 머지를 했습니다!!! 🎉🎉🎉🎉🎉

second error pr

정말 짜릿한 순간이였습니다. 퇴근하면서 머지 여부를 확인했을 때 나도 모르게 밖에서 예쓰!!! 를 외쳤고,
머지된 제 PR 과 master 브랜치에서 머지된 제 commit 을 봤을 땐 매우 행복했습니다 :D

master branch commit

여전히 남아있는 의문점

저의 첫 오픈소스 기여는 저의 PR 이 머지되어 마무리 되었습니다.
하지만 저는 개선, 해결해야할 문제가 있다고 생각합니다.
(물론 아닐 수도 있습니다! 디버깅 하면서 생각한 제 개인적인 의견입니다.)

두 번째 에러에 대한 문제 정의 과정에서 아래 내용이 기억나시나요?

controller 외 다른 기능에서도 비슷한 에러가 많았으며, controller 역시 동일 이슈가 있었습니다!

지금 Swiper 라이브러리 내에서 Swiper 객체가 destroyed 된 상태로 인해 발생하는 문제가 굉장히 많습니다.
이 중 대부분의 문제는 저와 같이 React 18 Strict mode 로 인해 발생한다는 것도 확인할 수 있었습니다.

이 문제는 destroyed 된 곳을 확인하고 early return 하여 처리할 문제가 아니라는
생각이 들었고 이건 단순히 여기저기 새는 곳을 막는 것 같다는 느낌을 받았습니다. 동시에
변경된 Strict mode 방식의 목적을 반영하여 해결하는 것이 맞을 것 같다는 생각이 들었습니다.
위에서 말한 목적은 React 는 구성 요소가 여러 번 마운트되고 제거되는 효과는 탄력적 이여야 한다는 리액트의 지향점으로 생각합니다.

이에 대한 디버깅은 시간이 될 때마다 진행하고 있습니다.
지금까지 얻은 정보는 useRef, 변경된 strict mode 이 두 가지가 섞이면서 발생한 이슈로 보입니다.
이슈일지 개발자의 의도일지는 더 확인해봐야알 것 같지만 시간이 될 때마다 디버깅 해볼 생각입니다!

마무리

이번 오픈소스 기여하는 과정에서 배운 것은 꽤 많았습니다.

Chrome 디버깅 툴 활용

크롬 디버깅 툴을 이렇게 잘 활용해본 것은 처음이였으며,
특히 conditional breakpoint 기능은 문제를 찾는 과정에서 큰 도움이 되었습니다.

보통 console 을 찍으면서 해당 값을 하나하나 확인하며 문제를 파악했는데
디버깅 툴을 통해서 처음 보는 오픈소스 임에도 불구하고 코드가 어떻게 흘러가는지
흘러가는 과정에서 당시 사용하는 값들이 어떤 상태인지, this 는 무엇이고,
현재 스코프 및 콜스택은 어떻게 되는지 등 많은 정보들을 볼 수 있었습니다.
그리고 이런 정보들은 문제를 파악하는게 큰 도움이 된다는 점 또한 알 수 있었습니다.

자신감

오픈소스에 이슈를 올리는 것 그 자체가 되게 어렵고, '내가 해도 괜찮을걸까?' 등 자신감이
부족해서 참여할 생각조차 못하고 있었으나, 이번 활동을 통해 자신감을 많이 얻을 수 있었습니다.
오픈소스라는 것이 정말 Top 급의 개발자 분들이 아니더라도 평범한 사람도
포기하지 않고, 이해하기 위한 노력을 들인다면 충분히 기여할 수 있다는 것 또한 느낄 수 있었습니다.
다들 한 번쯤 기회가 오면 망설이지 않고 해보는 걸 추천드려요!

React 공부

React 공부라고 말할 건 아니지만, React Strict mode 가 어떻게 변경되었고,
무슨 의도를 가지고 이런 변화를 가져갔는지를 알 수 있었습니다.

이전에는 strict mode 일 땐 두 번 실행되는구나 정도만 알고 지나갔으나
지금은 strict mode 일 떄 mount -> unmount -> mount 의 라이프사이클을 형성하고,
이 과정에서 내부 코드들이 한 번 더 실행된다. 이는 이러한 라이플사이클 과정에서
React 가 순수성을 가지고 동일하게 그리고 탄력적으로 실행되는지 여부를 알기 위해서이다.
라는 목적까지도 인지하게 되었습니다.

그리고 현재는 useRef 에 대해서 확인 중에 있습니다.
(아직까지 남아있는 의문점을 위해서 공부 중입니다.)

또한 저는 그저 문서나 책을 읽는 것보단 직접 그 문제와 이슈 속에 뛰어들어
경험하고, 그 속에서 문제 해결을 위해 찾아보는 과정에서 더 빠르게 이해하고 습득한다는 것을 알 수 있었습니다.

최종 후기

오픈소스 기여라는 것이 되게 가깝게 느껴집니다.
그리고 언제나처럼 문제를 정의하고, 풀어나가는 일련의 과정은 굉장히 즐겁다는 것 역시 느낄 수 있었습니다.
문제 해결에 절대적인 정답은 없고, 언제나 더 좋은 선택지를 찾기 위해 노력해야하며
이를 위해 계속 문제를 정의하고 좁혀나가고 이윽고 해결까지 도달하는 경험은 개발의 설렘을 가져다주었습니다.

내가 사용하고 있는 라이브러리의 contributor 가 되었다는 것
어쩌면 나와 비슷한 문제로 인해 고통받았을 여러 개발자들에게 보탬이 되었다는 것을 생각하면서
뿌듯함과 자신감을 많이 얻고 있습니다. :D

릴리즈 노트에 기입되어 있는 저의 이슈, PR 사진과 함께 이 포스팅을 마무리 지으려고 합니다.
긴 글 읽어주셔서 정말 감사합니다!

null issue release note

control issue release note