styled-components의 StyleSheetManager 캐싱 문제
문제 해결 과정을 공유합니다
Dino • Software Engineer / Web / Front
- Frontend
styled-components를 사용하며 iframe 내부에서 외부 바깥에 존재하는 DOM에 style을 주입하기 위해선, StyleSheetManager를 사용해야 합니다.
styled-components에는 캐싱을 위한 로직이 들어가 있고, StyleSheetManager를 사용할 때 이것이 문제를 일으킬 수 있습니다.
실제로 발생한 문제를 해결하면서 습득한 정보를 공유합니다.
문제 발생
푸시 메시지의 프로필 이미지(Avatar 컴포넌트)의 스타일이 적용되지 않는 버그가 발생했습니다! 😱
왼쪽: 정상, 오른쪽: 버그
DOM 에 eBGrkj 이라는 className은 잘 들어가 있으나, 스타일이 안 먹힌다는 것은 이 DOM이 참조할 수 있는 문서 범위 내에 해당 클래스 스타일이 존재하지 않는다는 것이었습니다.
배경
버그를 살펴보기 전에 채널톡의 기본적인 구조를 알아볼까요?
채널톡을 사이트에 설치하면 다음과 같은 구조가 생깁니다.
iframe으로 분리된 DOM들이 있고, 아닌 DOM들이 있죠. 버그가 발생한 PushMessage는 iframe의 바깥에 있습니다.
재현 조건
재밌는건, Avatar가 푸시 메시지에서 가장 먼저 마운트 되면 이 문제가 발생하지 않고, iframe 내부에서 똑같은 Avatar를 마운트하고 푸시 메시지가 왔을 때 이 문제가 발생하는 것입니다.
또한, iframe 내부에서 보이는 Avatar의 size만 바꿔줘도 해도 문제가 발생하지 않습니다.
힌트를 정리해보면 다음과 같습니다.
서로다른 html 문서(고객사, iframe)가 동시에 존재하고, 버그가 발생하는 Avatar는 양쪽 모두에서 쓰인다.
iframe 내부에서 Avatar가 먼저 쓰였을때만 문제가 발생한다.
양 측에서 쓰이는 Avatar 컴포넌트의 property가 똑같을 때만 문제가 발생한다.
구조 파헤쳐보기
styled-components 코드와 함께 보시면 좋습니다.
styled-componenets는 css를 이렇게 관리합니다.
styled-components className 캐싱
styled component로 생성한 컴포넌트는 각자 고유한 id를 갖고 있습니다. (css #id가 아님에 주의)
아래 사진의 Avatarstyled__Wrapper-yfy5xq-0 가 이 id입니다.
id는 className에 들어가지만, style은 없습니다. 실제 style은 eBGrkj, Bnwac 에서 넣어주고 있는데요.
상세 설명
eBGrkj는 prop에 의해 생성된 AvatarStyled > Wrapper의 스타일입니다.그리고
Bnwac는 Avatar를 override한 CommonStyled > Avatar의 스타일이죠.override한 순서에 따라 뒤쪽에 className을 넣어주어 우리가 의도한대로 작동합니다.
이렇게 id 뒤에 오는 style 들을 styled-components에서는
rules라는 이름으로 사용하고 있습니다.
styled-components 컴포넌트는 prop을 받아 다양한 variation을 만들 수 있습니다.
그래서 style-components는 컴포넌트를 prop에 따라 style을 생성한 후 해싱하여 이를 className으로 사용합니다.
이를 styleSheet에 저장하는데, 해당 해시가 이미 존재하면 추가하지 않습니다.
styled components는 prop에 의해 생성된 스타일이 같으면, 똑같은 해시를 가집니다.
위에서 발견한 힌트, '3. 양 측에서 쓰이는 Avatar 컴포넌트의 property가 똑같을 때만 문제가 발생함' 과 관련이 있어보이죠.
StyleSheetManager
styleSheet을 어디에 생성하는지는 src/sheet/dom.ts 를 보면 알 수 있습니다.
target을 정해주지 않으면, style 문서는 <head> 아래에 생성됩니다.
target은 StyleSheetManager(StyleSheetContext Provider를 관리함)에서 주입해줄 수 있는데요,
채널톡에서는 iframe 밖에 DOM을 렌더링하는 경우 StyleSheetManager를 쓰고 있습니다. iframe 밖에서는 내부의 styleSheet을 참조할 수 없기 때문이죠.
버그가 발생한 푸시 메시지도 마찬가지인데요. 다음과 같이 쓰고 있습니다.
그런데, ReactDom.createPortal() 내부에서 StyleSheetManager를 마운트하고 있죠.
여기서 문제가 발생했다고 의심해 볼 수 있습니다.
'createPortal에 의해 React DOM이 마운트됨과 동시에 StyleSheetManager도 마운트 되어 타이밍이 맞지 않는 걸까?'
그런데 이상한 점은 2번 힌트죠. 이미 Avatar를 마운트되어 있을 땐 iframe 내의 StyleSheet 을 참조하고, 새로 생성할 때는 고객사 html 아래에 제대로 생성이 된다는 점입니다.
이건 StyleSheet model의 구현을 보면 알 수 있습니다.
위에서 className 캐싱을 위해 사용한 메서드 hasNameForId 와 insertRules 의 차이를 봅시다.
친절한 주석 덕에 이해가 쉽습니다.
hasNameForId는 새로운 tag(:StyleSheetTag)에 대해 신경쓰지 않고, Model이 갖고 있는 멤버 변수 name만 사용합니다. (실제 tag를 불러와 읽으려면 시간이 오래 걸리기 때문이겠죠)
반면 insertRules에서는 tag를 lazy하게 가져와서 className을 insert하죠.
여기서 타이밍의 차이가 발생합니다.
createPortal에 의해 마운트되는 순간 StyleSheetContext에 의해 tag가 바뀌었는데, hasNameForId는 이전 StyleSheet의 값을 들고 있고, insetrRules는 새로운 StyleSheet 값을 가지는 것입니다.
해결 방법
타이밍 이슈가 발생하지 않게, PushMessage를 감싸는 StyleSheetManager를 미리 마운트 해두면 됩니다.
ReactDom.createPortal 바깥에, 항상 마운트되는 지점에 StyleSheetManager를 넣으면 되는거죠.
실제로 이렇게 수정을 하여 문제를 해결할 수 있었습니다.
문제를 해결하기 위해 Deep Dive 하는 채널톡 웹팀!
함께 하고 싶다면, 지금 바로 지원해주세요 🙌
