티스토리 뷰

iOS/UIKit

[UIKit] View Drawing Cycle

yanni13 2026. 1. 26. 22:02

 

 

 

오늘은 테이블뷰를 공부하던 중 imageView가 동그랗게 잘리지 않는 문제의 원인을 파악해 보기 위해

View Drawing Cycle에 대해서 공부해보려고 한다.

 

(제가 공부하기 위해 작성한 것이므로 여러 개념들이 나올 수 있으며 틀린 내용이 있다면 알려주세요!)


🤔 문제원인

self.profileImageView.layer.cornerRadius = self.profileImageView.frame.width / 2

 

분명 이미지 뷰의 넓이만큼 2로 나눠서 원의 형태로 이미지 뷰가 만들어지게 구현을 했지만 막상 빌드해 보면 아래 사진처럼 뭔가 마름모 모양의 형태로 구현이 되었다. 

 

근데 스크롤을 하다보면 또 정상적으로 원의 형태가 나오게 되어 실행 시점에 문제가 있는거 같아 DispatchQueue.main 안에 해당 코드를 넣어줬더니 아래와 같이 이미지뷰가 동그랗게 잘렸다.

 

그래서 View에서 프레임이 어느 시점에 결정되는지 고민해 보고자 view Drawing Cycle에 대한 개념을 학습하게 되었다.

 

 

 

✏️ View Drawing Cycle

뷰가 load되거나 reload 되면서 화면에 그려질 때 생기는 사이클을 의미

 

 

UIKit에서는 뷰를 그릴 때 아래와 같은 순서로 실행된다.

  1. constraint - 제약 조건 잡고
  2. layout - 레이아웃을 잡고
  3. draw - 뷰를 화면에 그린다.

스토리보드에서 오토레이아웃을 잡을 때를 생각해 보면 constraints를 먼저 결정하면 그 안에 frame 크기가 내부적으로 결정되고 그다음에 뷰가 화면에 그려진다.

 

이때 초기화 시점에서는 전체 사이클이 한번 실행되지만 다음 시점에서부터는 뷰가 그려질 때 이 사이클대로 전부 실행되는게 아니라 온디맨드 방식으로 사용자가 필요할 때 필요한 시점에만 실행하게 된다. 

 

만약 온디맨드 방식이 아니라 매번 이 사이클을 돌아야 하는 구조라면 ui가 변경되거나 다른 뷰를 왔다갔다 할 때마다 constraint부터 다시 계산하느라 엄청 성능이 떨어지고 비효율적일 것이다!

 

📍온디맨드 방식이란?
: 필요한 시점에 요청이 있을때만 실행되도록 하여 불필요한 계산을 줄이고 성능을 최적화하도록 설계된 방식이다.

 

 

 


코드로 살펴보자!

 

 

각 단계에서 어떤 메서드가 호출되는지 확인해보고자 간단한 코드를 작성해 보았다.

class TopView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("TopView - \(#function)")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 1. Constraints 변경 단계
    override func updateConstraints() {
        super.updateConstraints()
        print("TopView - \(#function)")

    }
    override func invalidateIntrinsicContentSize() {
        super.invalidateIntrinsicContentSize()
    }
    // 2. layout 단계
    override func updateProperties() {
        super.updateProperties()
        print("TopView - \(#function)")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.backgroundColor = .lightGray
        self.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
        print("TopView - \(#function)")

    }
    
    // 3. draw 단계
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        print("TopView - \(#function) rect: \(rect)")


    }
}

 

 

view drawing cycle에서 실행되는 주요 메서드는 5가지 종류가 있다.

  • updateConstraints
  • invalidateIntrinsicContentSize
  • updateProperties
  • layoutSubviews
  • draw

 

원래 4가지 정도였지만, iOS 26.0 버전부터 updateProperties가 추가되었다. 이 updateProperties는 view drawing cycle의 일부는 아니며 layout ~ draw 이전의 상태를 정리하기 위한 독립적인 프로퍼티 업데이트 단계로 추가되었다.

 

 

여기서 단계별로 크게 살펴보면 제약 조건이 변경되었을 때 실행되는 updateConstraints가 있고, intrinsic size를 기반으로 프레임이 변경되거나 제약조건에 대한 재계산 결과등 레이아웃이 변경됐을 때 실행되는 layoutSubviews, 뷰가 화면에 그려지는 과정으로 시각적인 결과가 변경될 때 draw메서드가 주로 사용되며 호출된다.

 

 

 

아주 간단한 코드 구현으로 버튼을 눌렀을 때 간단한 title 정도와 색상만 변경되도록 구성하였고 이 코드를 통해서 어느 시점에 실행되는지 파악해 보자.

 

 

uikit은 뷰가 그려질 때 constraint -> layout -> draw 순으로 실행하기 때문에 초기화 시점에서 setNeedsUpdateConstraints, setNeedsLayout, setNeedsDisplay와 같은 메서드로 다음 사이클에서 실행을 해달라고 예약하게 된다.

 

 



 

실제로 ViewController에서 제약 조건을 설정하고 constraint 변경 버튼을 눌렀을 때 topView.setNeedsUpdateConstraints() 메서드를 통해 constraint 계산이 필요할 수 있음을 알려줌으로써 updateConstraints가 실행되고 있는 걸 로그에서 확인해 볼 수 있다.

 

반면 frame과 bounds에 대한 변경은 이루어지지 않았으므로 layout 단계들은 실행되지 않는 것을 확인할 수 있다.

 

이게 바로 온디맨드 구조로 인해 변경된 사항에 한해서 적절한 메서드를 호출하게 되는 것이다.

 

 

 

여기서 다음 cycle에서 실행해 달라고 예약하는 방식으로 동작되는 메서드가 있다면 즉시 실행해달라고 하는 메서드들도 존재하지 않을까? 에 대한 궁금증이 생기게 되었다.

 

다음 순서에 실행되도록 예약을 하지 않고 바로 실행해달라고 요청하려면 어떻게 해야 할까?

 

그때 사용하는 게 layoutIfNeeded() 메서드이다.

 

@IBAction func clickedFrameButton(_ sender: UIButton) {
        
        print(#function, "layout 강제 업데이트")
        frameButton.setTitle("라벨ㅇㅇ 변경", for: .normal)

        UIView.animate(withDuration: 1.5) {
            self.view.layoutIfNeeded()
        }
    }

 

layoutIfNeeded()를 호출하면 다음 차례로 대기를 미루지 말고 즉시 실행해 달라고 요청한다.
보통의 경우 다음 사이클에 실행되어도 문제가 없지만, 변화되는 과정이 보여야 하는 애니메이션이나, 계산 로직과 같은 변경 과정이 사용자에게 보여야 하는 경우에는 다음 사이클을 기다리게 되면 눈에 띄지 않게 될 수 있다.

 

 

그 차이를 애니메이션을 통해 확인해 보자.

 

 

 🙆🏻‍♀️ 즉시 호출 (layoutIfNeeded 사용)

 

이렇게 약간의 애니메이션과 함께 label이 변경되는 걸 확인할 수 있다.

 

 

🙅🏻‍♀️ 즉시 호출 x (layoutIfNeeded 미사용)

 

똑같이 라벨이 변경되긴 하지만 애니메이션 적용 없이 라벨만 갈아 끼듯 변경된다는 것을 확인할 수 있다.

 

 

직접 해보기 전까진 왜 굳이 즉시 실행해야 하는지, 다음 사이클까지 기다려야 하는 거 아닌가 싶었는데 실제로 구현을 해봤을 때 변경사항이 보여야 하는 지점에서는 즉시 실행하라고 강제하지 않으면 변화가 보이지 않는다는 것을 확인할 수 있었다.

 

 

그럼 한 단계의 사이클로 구성된다 했으니까 제약 조건이 추가되거나 제거되었을 경우 updateConstraints부터 draw 메서드까지 전부 실행되는 걸까?

 

그렇지 않다!

 

 

제약조건이 추가되거나 제거되어도 layout단계에서 frame이 결정되는 영역에서 변화가 존재하지 않으면 Constraints 단계만 실행된다. 

 

예를 들어 라벨을 변경했다고 가정했을 때 라벨이 변하는 지점은 layout이 변하는 것이기 때문에 제약조건에 영향을 미치지 않아 constraint 관련 메서드는 실행되지 않는다. 여기까지는 아까 설명한 온디맨드 구조를 이해했다면 이해가 될 것이다!

 

주의할 점은 draw단계에서는 CoreAnimation에서 관리되기 때문에 항상 이벤트가 끝난 뒤 한 번에 렌더링 하도록 설계되어 있어서 다음 drawing cycle에서 실행된다. 즉 draw 단계는 즉시 실행되지 않게 된다.

 

 

 

문제 해결

 

그럼 다시 처음 문제로 돌아가서 imageView를 동그랗게 자르고 싶을 때 dispatchqueue를 사용해야 하는 이유가 무엇인지에 대해 살펴보자!

 

처음에는 frame의 크기가 layoutSubViews에서 결정되니까 layout부터 draw까지 순차적으로 실행될 건데, dispatchqueue를 적용해주지 않아도 순차적으로 실행되는 게 아닐까?라고 생각했었다.

 

하지만 애초에 오류가 났던 이유는 frame의 크기가 결정되지 않은 시점에서 접근했기 때문에 적용이 되지 않은 것이었다. 스크롤 하면 할수록 정상적으로 보였던 것은 테이블뷰 셀이 재사용되면서 layout이 다시 수행되면서 frame의 크기가 결정되었기 때문의 원의 형태가 유지되었던 것이였다.

 

DispatchQueue를 사용해서 시점을 미루는 것도 임시 해결방편이었고 결론적으로는 layoutSubViews에서 이미지 크기를 자르는 코드를 실행하는 방법으로도 해결해 볼 수 있다는 것을 알게 되었다.

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함