Flutter Clip이 뭐지? Clipbehavior 등.
포스트
취소

Flutter Clip이 뭐지? Clipbehavior 등.

Clip?

Clip이라는 용어를 처음 들었을 때 떠올랐던 건 문서를 정리하는데 쓰이는 클립이었습니다.

image-20240118182804224

이후 개발을 해 가면서 이미지의 가장자리를 둥글게 만들어야 할 일이 있었고,

ClipRRect위젯을 이용하며, 아! 클립이 자르는 걸 의미하는 느낌이구나 라는 걸 알게 되었습니다.

아마 아래의 뜻 중 4번의 의미로 쓰이는 느낌이었지요.

image-20240116203334423

더 개발을 해가며 ClipBehavior, CustomClip 등등을 변경하고 구현해보면서,

Stack에서 children의 그림자가 왜 잘릴까 생각해보면서, 점차 clip 뭔지 궁금해지기 시작해서 공부해보고 글을 정리 해 보았습니다.

Clip의 의미

Computer graphics에서 Rendering 하는 행위를 특정 구역으로 제한하는 것을 Clipping이라고 부른다고 합니다.

Flutter에서는 위젯을 렌더링할 때 부모와 자식 개념이 존재하고, 자식 위젯이 부모 위젯보다 커지는 경우, 혹은 자식 위젯이 그림자나 블러 등 특별한 효과를 가질 때 clip이 필요한 것 입니다.

그래서 ClipRRect등의 clipping을 사용하는 위젯에서 shadow가 있을 때,

부모 위젯의 크기에 따라서 그림자가 잘리거나,

때론 잘리지 않거나 하는 문제가 발생했던 것이었습니다.

image-20240118173753697

좌측은 ClipRect를 적용한 Container, 우측은 Clip이 없는 Container. 두 container모두 아래의 코드로 작성되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Container(
    height: 200,
    width: 200,
    decoration: BoxDecoration(
      color: Colors.red,
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.5),
          spreadRadius: 3,
          blurRadius: 30,
          offset: const Offset(0, 0),
        ),
      ],
    ),
  );

또, Clip은 곡선을 만들때 사용되기도 합니다.

종종 네모난 이미지나 위젯의 가장자리를 깎아 둥글게 만들어 주어야 할 때가 있습니다.

이럴 때 ClipRRect를 이용해서 구현할 수 있습니다.

ClipBehavior의 변경점.

Flutter 2.0 이전에는 상당수의 위젯에서 Clip을 사용했다고 합니다.

하지만 Clip을 사용하는것은 앱의 렌더링 성능을 크게 떨어뜨리고 있었습니다. 그 이유는 다음과 같았습니다.

과도한 saveLayer로 인한 성능저하

그 이전에는 Clip을 할 때 bleeding edge artifacts 문제를 해결하기 위해서 saveLayer를 사용했는데,

saveLayer 호출은 offscreen render target을 생성하고, 렌더 스크린 전환은 때때로 약 1밀리초의 비용이 들었다는 점 입니다.

image-20240118165434596

bleeding edge artifacts문제는 위의 사진으로 설명할 수 있는데,

자세히 들여다 보면 빨간색 테두리가 검정색 네모를 둘러싸고 있는 게 보입니다.

하지만 코드상으로는 해당 빨간색 빨간색 테두리는 rendering 되지 않아야 합니다.

부동소수점 오류 + 디바이스마다 픽셀의 밀도가 달라 생기는 오차 = bleeding edge artifacts 문제

렌더링이 의도와 다르게 되는 문제라고 할 수 있겠습니다.

flutter 팀은 이 문제를 해결하기위해서 렌더링 도중에 렌더링 하던 상태를 저장하고,

별도의 rendering 타겟(offscreen render target)을 생성하여 렌더링한 후 해당 타겟을 원래 렌더링 하던 타겟에 병합하는 방식으로

구현했다고 합니다. 이 과정이 구형 기기에선 큰 성능저하를 일으켰고 때떄로 1ms초의 비용이 들었는데

60fps가 나오기 위해선 약 16.7ms안에 렌더링이 끝나야 하니.. 굉장히 큰 비용이라고 볼 수 있었습니다.

Clipping 자체의 성능 저하

saveLayer 호출 없이도 clipping은 여전히 비용이 많이 듭니다.

왜냐하면 이는 복원될 때까지 모든 하위의 drawing에 적용되기 때문입니다.

따라서 내가 Clipping을 한번만 수행해도, 수백 개의 drawing에서 성능을 저하시킬 수 있는 문제가 있었습니다.

Clipping 통합과 기본 Clipbehavior 변경

위에 서술한 문제들 이외에도 clip이 위젯마다 따로 관리되는 문제가 있었기 때문에 전체적으로 통합하고

성능 향상을 위해 clip을 사용하지 않는 방법으로 구현될 수 있다면 해당 방식으로 구현하며 flutter 2.0 이후부터

다음 경우를 제외하면 대부분의 위젯에서 Clip.none으로 설정되었습니다.

1
2
3
4
5
6
7
8
9
- ClipPath defaults to Clip.antiAlias
- ClipRRect defaults to Clip.antiAlias
- ClipRect defaults to Clip.hardEdge
- Stack defaults to Clip.hardEdge
- EditableText defaults to Clip.hardEdge
- ListWheelScrollView defaults to Clip.hardEdge
- SingleChildScrollView defaults to Clip.hardEdge
- NestedScrollView defaults to Clip.hardEdge
- ShrinkWrappingViewport defaults to Clip.hardEdge

이상의 위젯은 기본적으로 clip을 수행하기 때문에 성능 향상을 꿈꾸고있다면..

해당 위젯들을 최소화 하는 것을 추천합니다.

Clip의 종류

Clipbehavior의 종류

위의 위젯들에서 hardEdge, antiAlias등이 눈에 띄었습니다.

어떤 경우에 어떤 clipbehavior를 사용해야 할까요?

  • 작은 곡선이 필요하다면

    1
    
    Clip.hardEdge
    
  • 부드러운 곡선이 필요하다면

    1
    
    Clip.antiAlias
    
  • 예전과 같은(saveLayer를 사용하는) 곡선이 필요하다면 -> 이 상황은 피하는 것이 좋습니다.

    • 위에 상술한 bleeding edge artifacts문제를 피해야 하는 경우.
    1
    
    Clip.antiAliasWithSaveLayer
    

    를 사용하면 됩니다.

Clip Widget

Flutter에서 개발자들이 주로 사용하는 clip을 구현해서 사용하기 쉽게 CustomClipper를 상속해서 만들어 준 4개의 위젯이 있습니다.

ClipRect ClipRRect ClipOval ClipPath 입니다.

  • ClipRect

    • 사각형으로 잘라줍니다.

    • 이 글의 맨 첫 사진처럼 painting 할 구역을 한정하는 느낌입니다.

    • 1
      2
      3
      
      ClipRect(
        child:  Image.network("https://picsum.photos/id/237/200/300"),
      ),
      
  • ClipRRect

    • 둥근 모서리로 잘라줍니다.

    • raidus인자로 받아 이미지의 모서리를 깎을 때 등, 다양하게 사용할 수 있습니다.

    • 1
      2
      3
      4
      5
      6
      7
      8
      
      ClipRRect(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(25.0),
          topRight: Radius.circular(25.0),
          bottomRight: Radius.circular(25.0),
        ),
        child: Image.network("https://picsum.photos/id/237/200/300"),
      )
      
  • ClipOval

    • 원 모양으로 잘라줍니다.

    • 원 모양으로 painting 할 구역을 한정합니다.

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      
      ClipOval(
        child: Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            color: Colors.red,
          ),
        ),
      )
      
    • image-20240118174103219

    • 무조건 원 모양으로 잘리는 게 아니라 가로세로 크기가 같지 않으면 타원형으로 잘립니다.
  • ClipPath

    • path를 주면 path 모양으로 잘라줍니다.

    • path를 주지 않으면 ClipRect와 동일하게 작동하나, ClipRect가 더 효율적입니다.

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      
      ClipPath(
        child: Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            color: Colors.red,
          ),
        ),
      );
      

CustomClipper

위의 4개의 clip widget으로도 원하는 결과를 얻지못할때도 있을 수 있습니다.

그럴 때 사용하는 것이 CustomClipper 를 상속하여 직접 클래스를 작성하는 것입니다.

CustomClipper를 상속하면 반드시 2개의 함수를 override해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
class CustomRect extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    throw UnimplementedError();
  }

  @override
  bool shouldReclip(covariant CustomClipper<Rect> oldClipper) {
    throw UnimplementedError();
  }
}

getClip, shouldReclip이 그 주인공입니다.

override 하는 함수들의 type들도 generic type에 따라 바뀌기 때문에

CustomClipper의 generic type을 지정하는것을 항상 조심해야합니다.

  • getClip
    • 인자로 오는 size는 렌더링되는 renderbox의 size입니다.
    • 이 size와 내가 원하는 clip을 짬뽕해서 Rect를 return해 주면 됩니다.
  • shouldReclip
    • clipper delegate가 변경되었을 때 clip을 다시 할 것인지 안할것인지 판단하는 함수입니다.
    • 만약 customRect 클래스가 다른 속성을 가지고, 속성이 변경될 때 마다 clip을 다르게 하길 원한다면
    • oldClipper의 해당 속성과의 비교를 통해서 다시 reclip할지를 정해준다고 생각하면 됩니다.

결론

clip이 뭔지 궁금해하는 것 부터 시작해서, clip behavior가 어떻게 바뀌었는지, 왜 clip하는데 비용이 비싼지 알아보고

clip을 사용하는 예시도 알아보는 글 이었습니다. 읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Flutter overlay 사용법

Invalid keystore format 오류와 keyhash, JAVA_HOME path 설정