Swift로 구현한 객관식 퀴즈 앱의 구조와 동작 흐름
📊 0. 앱 전체 흐름 플로우 차트
- 앱 실행 → viewDidLoad() 호출 → updateUI() 실행
- questionLabel에 문제 텍스트 표시, UIButton에 보기 세 개 출력
- 사용자가 선택지 버튼 클릭 → answerButtonPressed() 호출
- QuizBrain에서 정답 여부 확인 → 정답이면 녹색, 오답이면 빨간색 표시
- 0.2초 후 nextQuestion() 호출 → 다음 문제로 이동 or 리셋
- 점수와 진행률 표시, UI 갱신 반복
🧱 1. MVC 구조로 살펴보는 파일 구성
역할 파일 설명
역할 | 파일 | 설명 |
Model | Question.swift | 퀴즈 데이터 구조 정의 |
Model | QuizBrain.swift | 퀴즈 상태, 점수, 진행률, 로직 관리 |
View | Storyboard | UI 요소 정의 (버튼, 라벨, 프로그레스 바) |
Controller | ViewController.swift | UI 업데이트 및 사용자 입력 처리 |
🧩 2. Question.swift - 퀴즈 데이터 구조 정의
struct Question {
let text: String // 문제 텍스트
let answers: [String] // 선택지 배열
let rightAnswer: String // 정답 문자열
init(q: String, a: [String], correctAnswer: String) {
text = q
answers = a
rightAnswer = correctAnswer
}
}
🔍 문법 설명
- struct: 구조체. Swift에서 값 타입이며, 복사 방식으로 전달된다.
- let: 상수 선언. 변경 불가.
- [String]: 문자열 배열. 여러 개의 선택지를 순서대로 저장.
- 예시: ["심장", "피부", "대장"]
- 배열은 0부터 시작하는 인덱스로 접근: answers[0], answers[1] 등
- init(...): 사용자 정의 생성자. 인자 q, a, correctAnswer를 받아 프로퍼티 초기화
✅ 사용 예시
Question(q: "가장 큰 장기는?", a: ["심장", "피부", "대장"], correctAnswer: "피부")
🧠 3. QuizBrain.swift - 퀴즈 상태 및 로직 관리
struct QuizBrain {
var questionNumber = 0 // 현재 질문 번호
var score = 0 // 정답 점수
let quiz = [ // 문제 배열
Question(q: "...", a: ["A", "B", "C"], correctAnswer: "B"),
// 생략
]
func getQuestionText() -> String {
return quiz[questionNumber].text
}
func getAnswers() -> [String] {
return quiz[questionNumber].answers
}
func getProgress() -> Float {
return Float(questionNumber) / Float(quiz.count)
}
mutating func getScore() -> Int {
return score
}
mutating func nextQuestion() {
if questionNumber + 1 < quiz.count {
questionNumber += 1
} else {
questionNumber = 0
score = 0
}
}
mutating func checkAnswer(userAnswer: String) -> Bool {
if userAnswer == quiz[questionNumber].rightAnswer {
score += 1
return true
} else {
return false
}
}
}
🔍 문법 및 동작 설명
- mutating: 구조체 내부의 속성을 변경하기 위해 필요한 키워드
- 클래스(class)는 참조 타입이므로 mutating이 필요 없지만, 구조체(struct)는 값 타입이라 mutating을 사용하지 않으면 내부 속성 변경이 불가능하다.
- Float(...): 정수(Int)를 실수(Float)로 변환. UIProgressView에 필요
- quiz[questionNumber].answers: 현재 문제의 보기 배열 접근
- getAnswers() -> [String]: 문자열 배열 반환. 3개의 보기를 제공함
🎛️ 4. ViewController.swift - 사용자 입력과 UI 제어
@IBOutlet weak var questionLabel: UILabel!
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var choice1: UIButton!
@IBOutlet weak var choice2: UIButton!
@IBOutlet weak var choice3: UIButton!
@IBOutlet weak var scoreLabel: UILabel!
var quizBrain = QuizBrain()
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
@IBAction func answerButtonPressed(_ sender: UIButton) {
// 초기 버전에서는 아래처럼 옵셔널 강제 언래핑을 사용했음
// let userAnswer = sender.currentTitle!
// 이 방식은 버튼 타이틀이 nil일 경우 앱이 크래시날 수 있으므로 비추천
// 현재는 안전한 방식으로 개선됨
guard let userAnswer = sender.currentTitle else { return }
let userGotItRight = quizBrain.checkAnswer(userAnswer: userAnswer)
sender.backgroundColor = userGotItRight ? .green : .red
quizBrain.nextQuestion()
Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateUI), userInfo: nil, repeats: false)
}
@objc func updateUI() {
questionLabel.text = quizBrain.getQuestionText()
let answers = quizBrain.getAnswers()
choice1.setTitle(answers[0], for: .normal)
choice2.setTitle(answers[1], for: .normal)
choice3.setTitle(answers[2], for: .normal)
scoreLabel.text = "Score: \(quizBrain.getScore())"
progressBar.progress = quizBrain.getProgress()
choice1.backgroundColor = .clear
choice2.backgroundColor = .clear
choice3.backgroundColor = .clear
}
🔍 상세 문법 설명 및 예제 보완
- @IBOutlet, @IBAction: Interface Builder(UIStoryboard)와 연결된 코드 속성 및 액션
- UIButton.setTitle(_:for:)의 for: 파라미터는 UIControl.State 타입이며 버튼의 상태를 지정한다.
- .normal: 기본 상태 (항상 필요)
- .highlighted: 눌렸을 때 상태
- .disabled: 비활성화 상태
- .selected: 선택된 상태
✅ 실전 예제
choice1.setTitle("선택됨", for: .selected)
choice1.setTitle("비활성화됨", for: .disabled)
- guard let vs if let 비교:
// 안전하게 옵셔널 해제: guard
@IBAction func tapped(_ sender: UIButton) {
guard let title = sender.currentTitle else {
print("버튼 타이틀이 없음")
return
}
print("선택한 답: \(title)")
}
// 대안: if let 사용
if let title = sender.currentTitle {
print("답변: \(title)")
}
- !(옵셔널 강제 언래핑)는 가능한 피해야 하며, guard let 또는 if let을 사용하는 것이 더 안전하다.
📘 5. 문법 총정리 테이블
문법 요소 | 설명 |
struct | 구조체 선언. 값 타입, 복사 전달 |
let / var | 상수와 변수. let은 변경 불가, var는 수정 가능 |
[String] | 문자열 배열. 선택지를 저장하기 위해 사용 |
mutating | 구조체 내부 상태 변경 가능 메서드 지정 키워드 |
Float() | 정수 → 실수 변환. 프로그레스 바용 |
@IBOutlet | UI 컴포넌트 연결 속성 |
@IBAction | UI 액션 핸들러 연결 메서드 |
UIButton.setTitle(_:for:) | 버튼 타이틀 설정. 상태별(.normal, .highlighted, .disabled, .selected) 구분 가능 |
Timer.scheduledTimer | 일정 시간 후 특정 함수 실행. UI 피드백 UX 구성용 |
guard let / if let | 옵셔널 안전 해제 방식. 조건 분기 흐름 제어용 |
! (강제 언래핑) | 옵셔널이 nil일 경우 앱 크래시 발생. 실무에서는 비권장 |
✅ 정리
- 배열과 구조체를 통해 선택지를 유연하게 관리할 수 있다.
- MVC 패턴으로 코드가 역할별로 명확하게 나뉘어 관리와 확장이 쉽다.
- mutating 키워드는 구조체의 상태를 변화시키기 위해 필수이다.
- guard let과 if let은 옵셔널 값을 안전하게 해제할 수 있는 방식이며, 상황에 맞게 적절히 선택해야 한다.
- !는 옵셔널이 nil일 경우 앱 크래시를 유발할 수 있어 되도록 피하는 것이 좋다.
- 버튼 상태를 고려한 .normal, .highlighted, .disabled, .selected 등의 UI 상태 지정은 사용자 경험 개선에 매우 중요하다.
- 정답 확인 후 UI를 잠시 색상으로 피드백하고 다음 질문으로 자연스럽게 전환되는 UX는 Timer를 통해 구현된다.
반응형
'IT' 카테고리의 다른 글
바이브코딩 시대, AI와 함께 만드는 나만의 프로덕트 (0) | 2025.07.01 |
---|---|
iOS 앱에 MVC를 적용하는 구체적인 방법과 코드 예제 (1) | 2025.06.24 |
Swift 구조체와 MVC 패턴 완전 정복: mutating, 값 타입, 불변성 (0) | 2025.06.20 |
Swift 퀴즈 앱 리팩토링 : MVC 패턴, -> Bool, mutating func, _ 매개변수 (0) | 2025.06.19 |
Swift 앱 개발에서 struct로 배우는 코드 구조의 힘 (1) | 2025.06.18 |
댓글
이 글 공유하기
다른 글
-
바이브코딩 시대, AI와 함께 만드는 나만의 프로덕트
바이브코딩 시대, AI와 함께 만드는 나만의 프로덕트
2025.07.01 -
iOS 앱에 MVC를 적용하는 구체적인 방법과 코드 예제
iOS 앱에 MVC를 적용하는 구체적인 방법과 코드 예제
2025.06.24 -
Swift 구조체와 MVC 패턴 완전 정복: mutating, 값 타입, 불변성
Swift 구조체와 MVC 패턴 완전 정복: mutating, 값 타입, 불변성
2025.06.20 -
Swift 퀴즈 앱 리팩토링 : MVC 패턴, -> Bool, mutating func, _ 매개변수
Swift 퀴즈 앱 리팩토링 : MVC 패턴, -> Bool, mutating func, _ 매개변수
2025.06.19