챌린지 1-1 · 8 Topics

📱 화면에 뭔가 띄우기

"Hello World"를 앱에서 보고 싶다

View body VStack / HStack / ZStack modifier 체이닝 frame · padding · Spacer @main · App · Scene ScenePhase · onAppear Xcode Preview GeometryReader
시작 전에 던져볼 질문
01
SwiftUI에서 화면에 글자나 이미지를 띄우는 코드를 어떻게 쓰는가?
검색 → SwiftUI Text Image Button tutorial
02
여러 요소를 원하는 위치에 배치하려면 어떻게 하는가?
검색 → SwiftUI VStack HStack layout
03
작성한 코드가 실제 앱에서 어떻게 실행되는가?
검색 → SwiftUI @main App WindowGroup entry point
Topic 01

ContentView 구조

앱 시작점, struct가 View를 채택하는 이유
Swift
struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .font(.largeTitle)
            .foregroundStyle(.orange)
    }
}
생각해보기
  • SwiftUI에서 View를 class가 아닌 struct로 만드는 이유는 무엇인가?
  • some View에서 some을 빼면 어떤 에러가 나는가? 왜 필요한가?
  • #Preview와 시뮬레이터 빌드의 차이는 무엇인가?
struct ContentView: View
SwiftUI View는 struct다. class가 아닌 이유 — 값 타입의 불변성이 렌더링 시스템과 맞닿는 지점
var body: some View
View 프로토콜의 유일한 요구사항. some View — Opaque Type이 왜 필요한가
#Preview 매크로
iOS 17+ Preview 매크로. 빌드 없이 실시간 UI 확인. Canvas 활성화하는 법
Topic 02

기본 뷰 3종

Text · Image · Button
Swift
VStack(spacing: 16) {
    Text("개발자리")
        .font(.title)
        .bold()

    Image(systemName: "star.fill")
        .font(.system(size: 40))
        .foregroundStyle(.yellow)

    Button("시작하기") {
        print("탭!")
    }
    .buttonStyle(.borderedProminent)
}
생각해보기
  • Text("Hello").font(.title).bold()에서 .font().bold()의 순서를 바꾸면 결과가 달라지는가?
  • Image(systemName:)Image("이미지이름")의 차이는 무엇인가?
  • Button의 action 클로저 안에서 print()를 호출하면 Preview에서 출력을 볼 수 있는가?
Text("Hello")
문자열 표시. .font(), .foregroundStyle(), .bold() modifier 체이닝
Image(systemName:)
SF Symbols 사용. .resizable() + .scaledToFit() 조합. 크기 지정 패턴
Button(action:label:)
탭 액션 연결. 후행 클로저 문법 읽기. .buttonStyle() 커스터마이징
Topic 03

Stack 3종

VStack · HStack · ZStack — 화면을 쌓는 3가지 방향
Swift
VStack(spacing: 20) {
    HStack {
        Text("왼쪽")
        Spacer()
        Text("오른쪽")
    }
    .padding()
    .background(.gray.opacity(0.1))
    .cornerRadius(8)

    ZStack {
        RoundedRectangle(cornerRadius: 12)
            .fill(.blue.opacity(0.15))
            .frame(height: 60)
        Text("ZStack 겹침")
    }
}
생각해보기
  • VStack(alignment: .leading)에서 alignment를 생략하면 기본값은 무엇인가?
  • HStack 안에 Spacer()를 넣으면 어떻게 되는가? 두 개 넣으면?
  • .padding().background(.blue) vs .background(.blue).padding() 결과는 왜 다른가?
VStack — 수직 쌓기
alignment: .leading/.center/.trailing, spacing: 값 설정. 기본 alignment가 .center인 이유
HStack — 수평 쌓기
alignment: .top/.center/.bottom. 텍스트 baseline 정렬 .firstTextBaseline
ZStack — 겹치기
나중에 추가된 뷰가 위로. alignment 기준점. 오버레이 vs ZStack 선택 기준
Modifier 체이닝 순서
.padding().background() vs .background().padding() — 순서가 결과를 바꾸는 이유
Topic 04

frame modifier

크기를 직접 지정하는 방법
Swift
VStack(spacing: 12) {
    Text("고정 크기")
        .frame(width: 120, height: 40)
        .background(.orange.opacity(0.2))
        .cornerRadius(8)

    Text("최대 너비")
        .frame(maxWidth: .infinity)
        .padding()
        .background(.blue.opacity(0.15))
        .cornerRadius(8)
}
생각해보기
  • .frame(width: 200)은 뷰 자체의 크기를 바꾸는 것인가, 뷰를 감싸는 프레임의 크기를 바꾸는 것인가?
  • maxWidth: .infinity는 어떤 상황에서 쓰는가? 버튼을 화면 전체 너비로 만들려면?
  • .frame() 안의 alignment 파라미터는 무엇을 정렬하는가?
.frame(width:height:)
고정 크기 지정. 뷰 자체가 아닌 "뷰를 담는 프레임"에 크기를 주는 개념
maxWidth: .infinity
부모가 주는 최대 너비를 다 차지. 버튼 full-width 만들기 패턴
alignment 파라미터
frame 안에서 자식 뷰의 위치 결정. .frame(maxWidth:.infinity, alignment:.leading)
Topic 05

padding · Spacer · offset

여백과 위치 조정
Swift
VStack {
    Spacer()

    Text("가운데 정렬")
        .padding()
        .background(.orange.opacity(0.15))
        .cornerRadius(8)

    Spacer()
}
생각해보기
  • padding()에 값을 안 주면 기본 여백은 몇 pt인가?
  • Spacer()는 Stack 밖에서도 동작하는가?
  • .offset(x: 20).padding(.leading, 20)의 결정적 차이는 무엇인가?
.padding()
주변 여백 추가. .padding(.horizontal, 20) 방향별 지정. 레이아웃에 영향을 준다
Spacer()
남은 공간 전부 차지. Stack 안에서만 의미 있음. minLength 옵션
.offset(x:y:)
위치를 이동하지만 레이아웃 공간은 그대로 유지. padding과의 결정적 차이
Topic 06

GeometryReader

부모 크기 읽어서 반응형으로
Swift
GeometryReader { geo in
    HStack(spacing: 0) {
        Color.orange
            .frame(width: geo.size.width * 0.3)
        Color.blue
    }
}
.frame(height: 60)
.cornerRadius(12)
생각해보기
  • GeometryReader를 쓰면 왜 레이아웃이 예상과 다르게 변하는가?
  • 부모의 크기를 몰라도 되는 상황에서 GeometryReader를 쓰면 어떤 문제가 있는가?
  • 화면 너비의 30%만큼 너비를 주려면 어떻게 하는가?
GeometryProxy
geo.size.width, geo.size.height로 부모 크기 접근. 비율로 크기 계산
주의사항
GeometryReader는 최대 크기를 차지하려 한다. 의도치 않은 레이아웃 변화 주의
언제 쓰는가
화면 크기에 비례한 뷰, 스크롤 오프셋 읽기, 커스텀 애니메이션
Topic 07

@main · App · Scene · Lifecycle

앱의 진입점, Scene 구조, 생명주기 상태
Swift
@main
struct MyApp: App {
    @Environment(\.scenePhase) var phase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: phase) { _, newPhase in
            switch newPhase {
            case .active:
                print("앱 활성 — 화면 앞에 있음")
            case .inactive:
                print("비활성 — 전환 중")
            case .background:
                print("백그라운드 — 화면 뒤")
            @unknown default: break
            }
        }
    }
}

// View Lifecycle
struct HomeView: View {
    var body: some View {
        Text("홈")
            .onAppear {
                print("화면 나타남 — 데이터 로드")
            }
            .onDisappear {
                print("화면 사라짐 — 타이머 정지")
            }
    }
}
생각해보기
  • @main을 두 개의 struct에 붙이면 어떻게 되는가?
  • App → Scene → View 계층에서 각각의 역할은 무엇인가?
  • 앱이 백그라운드로 가는 시점에 저장 작업을 하려면 어디에 코드를 넣어야 하는가?
  • .onAppear는 View가 화면에 나타날 때마다 매번 실행되는가?
@main 어트리뷰트
진입점 마킹. 컴파일러가 main() 함수를 자동 생성. 하나의 타입에만 붙일 수 있다
App → Scene → View 계층
App은 앱 전체. Scene(WindowGroup)은 UI 컨테이너. View는 화면 조각. 이 순서대로 생성됨
ScenePhase — 앱 생명주기
.active(화면 앞) / .inactive(전환 중) / .background(화면 뒤). @Environment(\.scenePhase)로 읽음
onAppear / onDisappear — View 생명주기
onAppear: View가 화면에 나타날 때마다 실행 (NavigationStack 뒤로 후 다시 올 때도). 데이터 로드, 타이머 시작에 적합
Topic 08

View 계층과 레이아웃 영향

부모/자식 관계가 레이아웃에 미치는 영향
Swift
// 부모가 크기 제안 → 자식이 결정
VStack {
    Text("padding 먼저")
        .padding()
        .background(.orange.opacity(0.2))

    Text("background 먼저")
        .background(.blue.opacity(0.2))
        .padding()
}
생각해보기
  • SwiftUI에서 부모와 자식의 크기 협상은 어떤 순서로 이루어지는가?
  • 자식 View가 부모가 제안한 크기보다 더 큰 크기를 원하면 어떻게 되는가?
  • 시뮬레이터에서는 정상인데 실기기에서 다르게 보이는 경우는 언제인가?
크기 협상 과정
부모가 자식에게 제안 크기 전달 → 자식이 자신의 크기를 결정 → 부모가 배치. SwiftUI 레이아웃의 핵심
실기기 vs 시뮬레이터
렌더링 성능, Metal, 카메라/센서 차이. 시뮬레이터에서만 테스트하면 안 되는 이유
Preview
Hello, World!
Preview
개발자리 시작하기
Preview
왼쪽 오른쪽
ZStack 겹침
Preview
고정 크기
최대 너비
Preview
가운데 정렬
Preview
Console
앱 활성 — 화면 앞에 있음
// 홈 버튼 → 백그라운드
비활성 — 전환 중
백그라운드 — 화면 뒤

// 다시 열면
화면 나타남 — 데이터 로드
Preview
.padding().background()
padding 먼저
.background().padding()
background 먼저
Homework

챌린지 1-1을 마쳤다면

필수
자기소개 카드 만들기
VStack, HStack, ZStack을 모두 사용해서 이름, 사진(SF Symbol), 한 줄 소개가 있는 프로필 카드를 만들어보기. padding, background, cornerRadius modifier 활용
필수
modifier 순서 실험
.padding().background(.blue) vs .background(.blue).padding() — 두 가지를 나란히 놓고 결과가 왜 다른지 주석으로 설명 달기
도전
날씨 카드 UI 클론
ZStack으로 그라디언트 배경 위에 온도, 도시 이름, 날씨 아이콘을 겹쳐 배치. GeometryReader로 화면 너비의 90%로 카드 크기 설정
완료 체크리스트
Text, Image, Button을 코드 없이 떠올릴 수 있다
VStack/HStack/ZStack의 차이를 한 문장으로 설명할 수 있다
modifier 순서가 결과를 바꾸는 이유를 안다
@main → App → WindowGroup → ContentView 흐름을 그릴 수 있다
Solution — 답안 보기
1. 자기소개 카드 만들기
Swift
struct ProfileCard: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.orange.opacity(0.1))

            VStack(spacing: 12) {
                Image(systemName: "person.circle.fill")
                    .font(.system(size: 60))
                    .foregroundStyle(.orange)

                Text("홍길동")
                    .font(.title2)
                    .bold()

                HStack {
                    Image(systemName: "swift")
                    Text("iOS 개발자 지망생")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
            }
            .padding(24)
        }
        .frame(maxWidth: .infinity)
        .padding()
    }
}
2. modifier 순서 실험
Swift
VStack(spacing: 30) {
    // padding 먼저 → 여백 포함한 영역에 배경
    Text("padding → background")
        .padding()
        .background(.orange.opacity(0.3))
        .cornerRadius(8)

    // background 먼저 → 텍스트 크기만큼만 배경
    Text("background → padding")
        .background(.blue.opacity(0.3))
        .padding()
        .cornerRadius(8)
}
// modifier는 안→바깥 순서로 적용된다.
// .padding()이 먼저 오면 여백이 생긴 뒤 배경이 칠해지고,
// .background()가 먼저 오면 텍스트 크기만큼만 배경이 칠해진다.
3. 날씨 카드 UI 클론
Swift
struct WeatherCard: View {
    var body: some View {
        GeometryReader { geo in
            ZStack {
                LinearGradient(
                    colors: [.blue, .cyan],
                    startPoint: .top,
                    endPoint: .bottom
                )
                .cornerRadius(20)

                VStack(spacing: 8) {
                    Text("서울")
                        .font(.title2).bold()
                        .foregroundStyle(.white)
                    Image(systemName: "sun.max.fill")
                        .font(.system(size: 48))
                        .foregroundStyle(.yellow)
                    Text("24°")
                        .font(.system(size: 56,
                            weight: .thin))
                        .foregroundStyle(.white)
                }
            }
            .frame(width: geo.size.width * 0.9,
                   height: 200)
            .frame(maxWidth: .infinity)
        }
        .frame(height: 200)
        .padding()
    }
}
← Stage 1 목록 Stage 1 인덱스
코멘트
후원하기
콘텐츠가 도움이 됐다면
커피 한 잔의 후원을 부탁드려요 :)
카카오페이 QR
이현호
카카오페이
카카오톡 QR
막히면 여기서 질문하세요 💬
카카오톡 오픈채팅 · 무료 · 링크로 참여