Mui 오픈소스에 컨트리뷰팅 하게 된 썰

서혁준·2022년 7월 24일
1

🌊 TL:DR;

최근에 처음으로 오픈소스 컨트리뷰터가 되었다. MUI라는 라이브러리의 Next.js Example에 대해서 코드 수정을 했는데, 단순히 Typo 수정이 아니라 코드를 수정하게 된 컨트리뷰팅은 처음이다. 나한테 꽤나 큰 의미가 있던 일이기도 하고, 컨트리뷰팅을 하게 된 과정도 (내 생각에는) 재밌어서 블로그에 정리해보기로 했다.

🌊 발단

이 썰의 시작은 채널톡에서부터 시작한다. Mui에 컨트리뷰팅 한 썰인데 웬 채널톡?? 이라 생각할 수도 있겠지만 정말 그렇다.
회사에서 채널톡을 CS 용도로 사용하고 있었는데, 채널톡만 열었다가 닫으면 갑자기 CSS가 깨져버린다는 제보가 들어왔다.

💡 확인해보니 지금은 일어나지 않는 버그다! 채널톡 너무 좋은 서비스이니 많이많이 썼으면 좋겠다.

당시 회사 코드에서는 Next.js에 Material UI를 이용해서 서비스를 구현했다. Mui는 v5부터 CSS-in-JS 라이브러리인 emotion.js 를 내부적으로 사용하고 있다.

버그 상황을 설명해보면, 채널톡을 열었다가 닫으면 특정

처음에는 내가 MUI와 Next.js 관련 설정을 잘못해서 나는 버그였을까? 라고 생각했다. 그래서

https://github.com/mui/material-ui/tree/master/examples/nextjs

에서 Mui + Next.js 기본 Example을 가지고 와서 채널톡을 달아서 실험을 해봤다.
그랬는데 또다시 동일한 버그가 일어났다!

다행히도(?) 내 실수가 아니라는 것을 깨닫고, 이 버그를 재현할 수 있는 레포와 함께 채널톡 팀에 제보를 넣었다. (하지만 내 실수가 아닐경우 더 심각한 문제를 발견할 것일수도 있다... )

https://github.com/ANTARES-KOR/channeltalk-bug-reproduce

역시 채널톡을 이용해서 제보가 이루어졌는데 빠르게 개발자분을 연결해주시고, 답변주시는 모습이 인상깊었다.

채널톡 팀에서는 createEmotionCache.js 코드가 의심스럽다고 말씀하셨다.

그래서 해당 코드를 가져와 봤다.

import createCache from '@emotion/cache';
// prepend: true moves MUI styles to the top of the <head> so they're loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export default function createEmotionCache() {
  return createCache({ key: 'css', prepend: true });
}

이 코드는 Emotion의 스타일 태그들이 어떻게 HTML 문서에 삽입되는 방식을 커스터마이징하기 위한 코드이다. (설명) 주석에도 나와 있듯이, MUI의 스타일을 다른 스타일링 솔루션으로 Override하는것이 가능하게 하기 위해서 prepend 속성을 이용해 <head/> 태그 맨 앞에 스타일이 들어가도록 한다.

이 코드를 제거하니깐 작동이 잘 되었다고 한다. 하지만 이 코드를 어떻게 제거했는지, 그리고 제거했을 때의 문제가 어떤 문제가 발생할지 모르겠어서 그 방식을 그대로 사용할 수는 없었다.

그래서 createEmotionCache 코드를 보니 다음과 같은 JsDoc notation이 있었다.

prepend가 deprecated 되어서 문제가 생긴건가? 하는 생각이 들었다. 그래서 prepend를 삭제하고 실행해 보니 버그가 해결되었다!

소 뒷걸음치다 쥐잡은 격이긴 한데, prepend 옵션 자체가 문제는 아니지만 이 옵션을 사용하지 않음으로써
<head/> 태그 내에서 <style/> 태그가 삽입되는 위치가 변경되었고, 이 위치의 변화로 인해서 채널톡의 관련 코드와 충돌이 일어나지 않아서 해결된 것으로 보인다.

주석을 확인해 보면 prepend 옵션을 사용하는 이유가 MUI 스타일을 다른 스타일링 방식이 오버라이딩 하기 쉽게 하도록 사용한다고 되어 있는데, 우리 서비스에서는 다행히 Emotion 외의 방식을 사용하지 않아서 prepend를 지워도 괜찮았다.

여튼 이렇게 급한 일은 해결되었다. 하지만 Mui에서 공식적으로 제공하는 Next.js Example에 deprecated 되는 코드가 들어있다는 것이 좋지 않은 것 같아서 관련해서 수정을 요청했다.

🌊 Mui에 Issue 생성

우선 prepend 를 deprecate 시킨 PR부터 찾아보았다.

해당 PR에 보면 스타일 규칙을 삽입하는 위치를 좀 더 커스텀하게 지정할 수 있게 하기 위해서 해당 옵션을 구현하게 되었다라고 적혀있다. 스타일 규칙은 나중에 온 스타일 규칙이 앞의 스타일 규칙을 Override하기 때문에 EmotionCache를 사용할 때 유저가 직접 스타일 규칙이 삽입되는 위치( = 순서 ) 를 정할 수 있게 된다면 좀 더 예측 가능한 방식으로 스타일을 적용할 수 있기 때문이다.

그래서 Mui의 Next.js Example에서 prependinsertionPoint로 변경해달라는 이슈를 생성했다.

내용을 보면 "SSR에서 내가 구현해보려고 했는데 하기 어려워서 할줄 아는 다른 사람이 해줬으면 좋겠다~" 라고 써져 있다. prepend 옵션의 경우 <head/> 태그 제일 앞에다 넣으면 되지만, insertionPoint의 경우 document 내에서 <style/>을 뒤에 삽입할 태그를 찾아줘야 한다.

하지만 SSR은 HTML을 서버에서 생성하게 되는데 서버는 Browser 환경이 아니므로 Web API를 사용할 수 없다. document.findElementById 등을 이용할 수 없다는 말이다. 따라서 클라이언트 사이드에서 해당 태그를 찾아서 삽입하도록 해줘야 한다.

관련 질문에 대한 Emotion.js 메인테이너 분의 답변.

이 당시에 나는 Next.js에서 어떤 코드가 서버 사이드에서 돌아가는 것이고, 어떤 코드가 클라이언트 사이드에서 돌아가는것인지에 대한 감이 없어서 잘 아는 사람이 해줬으면 좋겠다~ 하고 썼다.

🌊 직접 코드 수정하기

해당 이슈를 생성하고 며칠이 지났지만, 놀랍게도 아무도 관심이 없었다...!

많은 걸 느낀 대목인데, 오픈소스를 돌다 보면 생각보다 A라는 라이브러리를 관리하는 사람이 B라는 라이브러리도 관리하고 있다는 걸 볼 수 있다. 그만큼 오픈소스 관리하시는 분들은 할 게 많다. 그래서 이렇게 애매한 작업은 할 유인이 부족한 것 같다.

다행히도 insertionPoint 코드를 직접 작성하신 분께서 누가 PR 작성하면 도와주겠다 라고 하셔서 에라 모르겠다 하고 내가 해볼게~ 해버렸다.

Server-side? Client-Side?

React 기반의 SSR은 ReactDOMServer가 제공하는 renderToString() 과 같은 함수들을 이용한다.
서버단에서 JSX를 평가해서 미리 HTML Markup을 생성한 뒤 이를 클라이언트에 전달하기 때문이다.

function handleRender(req, res) {
  const cache = createEmotionCache();
  const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

  // Render the component to a string.
  const html = ReactDOMServer.renderToString(
    <CacheProvider value={cache}>
      <App />
    </CacheProvider>,
  );

  // Grab the CSS from emotion
  const emotionChunks = extractCriticalToChunks(html);
  const emotionCss = constructStyleTagsFromChunks(emotionChunks);

  // Send the rendered page back to the client.
  res.send(renderFullPage(html, emotionCss));
}
간단한 SSR 예시. ReactDOMServer API를 이용해 생성한 HTML을 response로 리턴하는 것을 확인할 수 있다.

결국 Next.js에서 작성한 JSX는 Server-Side에서 평가되어 HTML로 생성된다. HTML을 Server Side에서 생성하는 과정에서 CSS가 없으면 FOUC가 발생하기 때문에 내가 수정해야할 createEmotionCache의 코드는 서버 사이드와 클라이언트 사이드에서 모두 평가된다고 볼 수 있겠다.

💡FOUC란?
"Flicker Of Unstyled Content" 의 약자로서, 외부의 CSS가 불러와지기 전 스타일이 적용되지 않은 페이지가 나타나는 현상이다.

CSS-in-JS 방식의 스타일링 라이브러리 (styled-components, emotion.js)는 자바스크립트를 이용해서 CSS 스타일을 적용시키는데, 브라우저에서는 HTML+CSS만을 이용해 먼저 유저에게 콘텐츠를 보여주고 자바스크립트를 로딩하므로 해당 문제가 발생하기 쉽다. (참고)

따라서 CSS-in-JS 라이브러리를 이용하는 경우에는 미리 style을 추출해내서 스타일시트를 생성하는 작업이 필요하다. 이 경우에서는 createEmotionCache가 해당 역할을 하는 코드중 하나라고 볼 수 있겠다.

✏️ createEmotionCache.js

import createCache from '@emotion/cache';

const isBrowser = typeof document !== 'undefined';

// On the client side, Create a meta tag at the top of the <head> and set it as insertionPoint.
// This assures that MUI styles are loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export default function createEmotionCache() {
  let insertionPoint;

  if (isBrowser) {
    const emotionInsertionPoint = document.querySelector('meta[name="emotion-insertion-point"]');
    insertionPoint = emotionInsertionPoint ?? undefined;
  }

  return createCache({ key: 'mui-style', insertionPoint });
}

해결책은 간단했다. 현재 이 코드가 실행중인 환경이 브라우저인지, 아닌지에 대한 판단을 하는 코드를 넣고 브라우저인 경우에 Web API를 이용해서 insertionPoint를 지정해주면 되었다.

그래서 이렇게 코드를 구현해서 PR을 넣었다.

🌊 이후..

사실 이 이후의 과정이 순탄하지는 않았다. Material-ui가 스타일을 삽입하는 방식이 Next.js (+ react 18) 과 충돌을 일으키면서 문제가 발생했다.

그래서 해당 문제가 해결되기까지 3개월? 가량의 기다림의 시간이 필요했다. 그리고 Next.js 12.2에서 해당 문제가 해결되면서 지난주? 에 드디어 첫 컨트리뷰팅이 마무리되었다.

오픈소스에 참여할 기회를 잡으려면 많은 노력(코드를 자주 뜯어본다던가..)이 필요하지만, 이렇게 작은 기여를 하면서도 상당히 많은 지식을 얻을 수 있었다. 그리고 한국에 사는 입장에서 MS출신 엔지니어에게 무료로(?!) 코드리뷰를 받을 수 있다는 장점도 있다!

그리고 내가 짠 코드가 다른 사람의 프로젝트 안에서 돌아간다는 쾌감은 개발자로서 살아가는 이유가 아닐까?

내가 작성한 PR은 여기 에서 확인해볼 수 있다.

profile
방구석에 앉아 미래를 상상하는 나

0개의 댓글