반응형
SwiftUI Study – body 안에서 무거운 연산을 실행하지 않는 이유와 해결법
1. 왜 중요한가 (문제 배경)
SwiftUI의 body는 상태가 변경될 때마다 반드시 다시 호출되는 함수입니다.
즉, body는 다음과 같은 순간마다 재실행됩니다.
- @State 값 변경
- @Binding 변경
- ObservableObject 업데이트
- 환경 값(Environment) 변경
- 부모 뷰 재렌더링
- 애니메이션 중간 프레임
이 뜻은 곧 다음을 의미합니다.
body 안에서 무거운 연산을 실행하면 성능이 즉시 하락한다.
대표적인 문제 상황
- 정렬(sort), 필터(filter), 매핑(map)
- JSON 디코딩, 날짜 포맷팅(DateFormatter)
- 이미지 리사이징
- 네트워크 요청 or 블록킹 I/O
- 비싼 계산(루프, 알고리즘 등)
body는 “뷰 선언”만 담당해야 하며, “연산/로직”을 포함해서는 안 됩니다.
2. 잘못된 패턴 예시
❌ 예시 1: body 안에서 정렬/필터 실행
struct WrongListView: View {
let items: [Item]
var body: some View {
List {
ForEach(items.sorted(by: { $0.score > $1.score })) { item in // ❌ 정렬이 매번 수행됨
Text(item.title)
}
}
}
}
문제점
- body가 호출될 때마다 정렬 재실행
- 데이터 개수가 많아지면 렌더링 지연 발생
- 스크롤 시에도 반복 실행되며 프레임 드랍 발생 가능
❌ 예시 2: 날짜 포맷터 매번 생성
struct WrongDateView: View {
let date: Date
var body: some View {
let formatter = DateFormatter() // ❌ 비용 큼
formatter.dateFormat = "yyyy-MM-dd"
return Text(formatter.string(from: date))
}
}
문제점
- DateFormatter는 매우 비싼 객체
- body 재호출마다 반복 생성 → 불필요한 CPU 소모
❌ 예시 3: body 안에서 네트워크 요청
struct WrongNetworkView: View {
@State private var result: String = ""
var body: some View {
VStack {
Text(result)
}
.onAppear {
load() // ❌ 하지만 load 안에서 무거운 작업을 동기 실행하면 문제
}
}
func load() {
let data = try! Data(contentsOf: URL(string: "https://...")!) // ❌ 동기 I/O
result = String(decoding: data, as: UTF8.self)
}
}
문제점
- 동기 네트워크는 UI 스레드를 블록
- body와 무관한 작업이 UI 성능을 직접 저하시킴
❌ 예시 4: 이미지 리사이징/연산을 body 안에서 수행
Image(uiImage: image.resize(width: 300)) // ❌ 매번 resize 재실행
문제점
- 스크롤 리스트 안에 있을 경우 성능이 참혹하게 떨어짐
- 연산이 반복 수행되며 CPU 사용률 증가
3. 올바른 패턴 예시
✅ 예시 1: 연산은 미리 계산한 property로 분리
struct CorrectListView: View {
let items: [Item]
var sortedItems: [Item] {
items.sorted(by: { $0.score > $1.score }) // body 밖에서 계산
}
var body: some View {
List(sortedItems) { item in
Text(item.title)
}
}
}
장점
- body는 단순히 렌더링만 담당
- SwiftUI diffing과 독립적
- 성능 저하 없음
✅ 예시 2: DateFormatter는 정적(static)으로 캐시
struct CorrectDateView: View {
let date: Date
static let formatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"
return f
}()
var body: some View {
Text(Self.formatter.string(from: date))
}
}
장점
- DateFormatter는 1번만 생성
- body 호출은 단순 string 사용만 수행
- 성능 안정적
✅ 예시 3: 비동기 연산은 task / onAppear + async로 분리
struct CorrectNetworkView: View {
@State private var text = ""
var body: some View {
Text(text)
.task {
text = await load()
}
}
func load() async -> String {
let (data, _) = try! await URLSession.shared.data(from: URL(string: "https://...")!)
return String(decoding: data, as: UTF8.self)
}
}
장점
- body가 비동기 작업과 분리됨
- UI 스레드 차단 없음
- 상태 변경 흐름도 안정적
✅ 예시 4: 이미지 연산은 ViewModel 또는 background task에서 수행
@MainActor
final class PhotoViewModel: ObservableObject {
@Published var resized: UIImage?
func process(_ image: UIImage) {
Task.detached {
let result = image.resize(width: 300) // 백그라운드 연산
await MainActor.run {
self.resized = result
}
}
}
}
body에서는 단순히:
if let img = viewModel.resized {
Image(uiImage: img)
}
4. 실전 적용 팁
✔ 팁 1 – “body는 절대로 계산 로직을 포함하지 않는다”
body는 화면 선언만 담당해야 하며,
연산/필터/정렬/변환/날짜처리 등은 모두 별도 프로퍼티나 메서드로 이동.
✔ 팁 2 – ViewModel이 존재한다면 연산은 대부분 ViewModel로 이동
SwiftUI는 UI 선언, ViewModel은 연산/로직이라는 역할이 명확해짐.
✔ 팁 3 – 정렬/필터는 body 바깥에서 수행하면 성능이 확연히 좋아짐
특히 리스트에서 필수.
✔ 팁 4 – 애니메이션 중에는 body가 수십 번 호출된다
→ body 안에 연산이 있으면 초당 수십 번 반복 실행됨
→ 프레임 드랍 / 스크롤 끊김 발생
✔ 팁 5 – DateFormatter, NumberFormatter, Regex 등은 static 캐시가 정석
가장 비용이 큰 객체들 중 하나.
5. 정리
- body는 선언적 UI의 “그리기 청사진”일 뿐이며, 연산 로직이 들어가면 성능이 급격히 떨어진다.
- 정렬/필터/매핑, 이미지 처리, 날짜 포맷팅, 네트워크 등은 절대 body 안에 넣지 않는다.
- 가능한 모든 계산은 body 외부에서 수행하거나 ViewModel로 이동한다.
- 이 원칙을 지키면 SwiftUI의 성능 문제 절반은 자동으로 해결된다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – @StateObject와 @ObservedObject를 정확하게 선택하는 법 (뷰 재생성·초기화 문제 방지) (0) | 2025.12.09 |
|---|---|
| SwiftUI Study – NavigationStack을 안정적으로 설계하는 법 (중복 Push·잘못된 경로·상태 꼬임 방지) (0) | 2025.12.09 |
| SwiftUI Study – 이미지 로딩·캐싱·리사이징을 효율적으로 처리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – ScrollView 안에서 LazyVStack을 사용해 성능을 최적화하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – AnyView 남용을 피하고 타입 안정성을 유지하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – View 중첩이 깊어지지 않도록 컴포넌트를 분리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – offset / position으로 레이아웃을 잡지 말아야 하는 이유 (0) | 2025.12.09 |
| SwiftUI Study – GeometryReader에 의존하지 않고 레이아웃을 안정적으로 구성하는 방법 (0) | 2025.12.09 |

