React Native UI 트러블 슈팅 (1. 천천히 사라지는 의문의 박스?)

eeennsu·2026년 6월 20일

React Native

목록 보기
54/98

React Native에서 비대칭 borderRadius가 만드는 한 프레임 잔상

가로 스크롤 리스트의 필터 탭을 전환할 때, 화면 좌상단으로 작은 검은 박스가 한순간 사라지는 잔상이 보였다. 원인은 리스트 라이브러리가 아니라, 한 박스에 비대칭 borderRadius와 배경색을 함께 적용한 것이었다. 이 글은 균등 borderRadius와 비대칭 borderRadius의 네이티브 렌더링 차이를 중심으로 정리한다.

증상

  • 가로 스크롤 리스트 위에 카테고리 필터(pill 탭)가 있는 화면.
  • 카테고리를 전환하면 화면 좌상단으로 작은 검은 박스가 빠르게 사라지는 잔상이 나타남.
  • 빈 카테고리 → 데이터가 있는 카테고리로 전환할 때만 발생. 데이터 → 데이터, 데이터 → 빈 전환에서는 없음.
  • iOS·Android 공통.

문제의 박스는 이런 스타일을 갖고 있었다.

// 문제 패턴 — 비대칭 radius + 배경색을 같은 View에 적용
<View
  bg="rgba(11, 13, 15, 0.9)"   // 거의 검정
  height={26}
  px={8}
  borderBottomRightRadius={8}   // 네 모서리 중 하나만 둥글게
>
  <Text>Benefits+</Text>
</View>


근본적인 원인 : 모서리가 균등할 때와 그렇지 않을 때의 차이

네 모서리가 같은 값인 borderRadius와, 한 모서리만 둥글게 준 비대칭 radius는 네이티브가 그리는 방식이 다르다.

모서리가 균등할 때는 그리기 쉽다. iOS·Android 모두 "이 사각형을 이만큼 둥글게"라는 명령을 그래픽 시스템이 곧바로 알아듣는다(iOS의 cornerRadius 등). 추가 계산 없이 한 번에 그려지므로, 화면에 나타나는 순간부터 올바른 모양과 위치로 보인다.

모서리가 제각각일 때는 한 번에 못 그린다. "한 모서리만 둥근 도형"은 단일 명령으로 표현할 수 없어서, 네이티브가 그 모양의 윤곽선(path)을 직접 그린 뒤 그 선대로 잘라내는(clipping) 작업을 해야 한다. 이 잘라내기를 위해 별도의 레이어를 따로 만들어 합치는 단계까지 더해진다. 즉 균등한 모서리보다 손이 더 가는 처리다.

여기서 핵심은, 이 추가 처리가 컴포넌트가 화면에 처음 올라오는(mount) 그 첫 순간에는 아직 안 끝나 있을 수 있다는 점이다.

균등 모서리는 GPU가 수식만으로 즉시 처리하는 빠른 방법을 쓰지만, 이 방법은 네 모서리 값이 같을 때만 쓸 수 있다. 모서리가 제각각이면 그 모양의 마스크를 따로 만들어 잘라내는 단계가 하나 더 많은 범용적인 플로우를 탄다. 네이티브의 이 과정이 '느린' 게 아니라, 비대칭일 때만 추가 마스크 단계가 끼는 것뿐이다. 웹도 똑같이 비용을 내지만, 브라우저 엔진이 워낙 빠르고 "다 그린 뒤 보여주는" 구조라 그리다 만 중간 상태가 눈에 안 보인다.



왜 잔상으로 보였을까?

비대칭 radius를 가진 뷰가 처음 mount될 때, 네이티브가 잘라내기 계산을 마치기 전에 첫 프레임이 한 번 그려질 수 있다. 이때 뷰는 제 위치를 아직 못 잡아서 화면 좌상단 구석(원점, 0·0 부근)에 잠깐 그려졌다가, 바로 다음 프레임에 원래 자리로 점프한다. 모바일 한 프레임은 약 16.7ms(60fps)라서, 이 한 프레임짜리 위치 점프가 눈에는 "좌상단에서 무언가 스르륵 빠지는" 잔상으로 보인다.

여기에 배경색까지 들어가 있으니, 잠깐 엉뚱한 자리에 그려진 그 뷰가 색을 가진 박스로 인지됐다. 색이 검정이라 눈에 확 띄었을 뿐, 옅은 색이었다면 같은 현상이라도 못 보고 지나갔을 것이다.

즉 "둥글게 안 잘려서" 문제가 아니라, 잘라내기 처리가 한 프레임 늦는 바람에 뷰가 잠깐 엉뚱한 위치에 나타난 것이 잔상의 정체다.



왜 특정 상황에서만 나타났는가

빈 → 데이터 전환에서만 보인 이유 — 가로 리스트는 셀을 재사용(recycle)한다. 데이터 → 데이터로 바뀔 때는 기존 셀을 그대로 다시 쓰기 때문에 새로 mount되지 않고, 네이티브 뷰도 다시 그려지지 않으니 잔상이 없다. 반면 빈 → 데이터에서는 카드 셀이 처음 mount되면서 네이티브 뷰가 처음으로 그려지고, 그제서야 비대칭 radius의 잘라내기가 한 프레임 늦는 현상이 드러난다. 다시 말해 잔상을 부른 건 탭 클릭 자체가 아니라, 클릭으로 인해 새 셀이 처음 mount되는 타이밍이다.

이 화면에서만 보인 이유 — 문제의 박스를 가진 컴포넌트를 쓰는 곳이 이 화면뿐이었다. 같은 필터 탭을 쓰는 다른 화면들은 이 컴포넌트를 쓰지 않아 애초에 영향이 없었다.




해결 : radius와 배경색을 다른 View로 분리

비대칭 radius와 배경색을 한 View에 같이 두지 않고 분리한다.

  • 바깥 View: overflow: hidden + 비대칭 radius. 배경색 없음. 클리핑만 담당한다.
  • 안쪽 View: 배경색 + 콘텐츠. 모서리가 균등한 평범한 사각형이라 별도 레이어 합성이 없다.
// 해결 패턴 — radius는 바깥, 배경색은 안쪽
<View overflow="hidden" borderBottomRightRadius={8}>
  <View
    bg="rgba(11, 13, 15, 0.9)"
    height={26}
    px={8}
    items="center"
    justify="center"
  >
    <Text>Benefits+</Text>
  </View>
</View>

바깥 View가 모양대로 잘라주고, 안쪽 View는 배경색을 가진 단순 사각형이라 첫 프레임 어긋남을 만드는 조합 자체가 사라진다.



규칙

비대칭 borderRadius와 backgroundColor를 같은 View에 동시에 적용하지 않는다. radius는 overflow: hidden을 가진 바깥 View에, 배경색은 균등한 사각형 안쪽 View에 둔다.

특히 다음 조건이 겹치면 잔상이 두드러진다.

  • 부모가 transform 애니메이션을 가진 컨테이너 (가상화 리스트의 가로 recycle, Reanimated 기반 셀 등)
  • 자식이 빠르게 mount/unmount 되는 시나리오 (리스트 신규 mount, 필터 전환)
  • 배경색이 눈에 잘 띄는 색


쉽지 않았던 디버깅 과정..

이 잔상은 좌상단으로 빨려 들어가는 모습 때문에 가상화 리스트가 화면 밖 셀을 극단적 좌표로 밀어내는 동작을 강하게 의심하게 했다. 리스트 리마운트, layout shift, 빈 상태 컴포넌트, 이미지 crossFade 등 상위 레이어 가설을 여러 번 세웠지만 모두 아니었다

원인을 특정한 결정적 방법은 단순했다. 의심되는 박스의 배경색을 bg="red"로 바꿔보니 좌상단 잔상도 빨갛게 변했다. 잔상의 색·크기·위치가 특정 View의 시각 속성과 일치한다면, 라이브러리 내부보다 그 View 자체를 먼저 검증하는 것이 빠른 것 같다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글