본문 바로가기
개인공부

[iOS/Xcode] 기초 연습문제를 통한 프로그래밍 이해하기 4편

by 싸디아재 2025. 3. 25.
728x90
반응형

 

6. 함수 shuffled() 를 활용하여 랜덤 퀴즈 출제
출처: https://dailyblessing.tistory.com/228 [싸디아재의 시선과 흐름:티스토리] 이전 글

 

 

어제는 shuffled() 함수를 사용하여 퀴즈를 랜덤하게 출제하는 것까지 구현을 해 보았다. 사실 이보다 진도가 더 나갔었는데 시간이 너무 늦어 포스팅을 미뤘다. 어제에 이어서 계속해서 퀴즈앱을 쭉 진행해보자.

또 모르지 않겠는가? 이 앱이 텍스트 어드벤처 개발로 이어질지... ZORK 추억의 게임이다.

 

1977년도 처음 출시가 되었더랬다. 내가 태어나기도 전에 만들어졌던 게임... 국민학교를 들어가서야 ZORK I을 접했던 것 같다.

재미있었는데...

 

오늘은 아래와 같이 새로운 함수와 기능을 추가하려고 한다.

 

목차

7. prefix() / Array() / ArraySlice<Int> / startIndex, endIndex

8. 오답 복습 기능 만들기 append()

9. 

 

7. prefix() / Array() / ArraySlice<Int> / startIndex, endIndex

prefix란 배열에서 앞에서부터 n개만 꺼내는 함수라고 한다. 예를 들어보자.

let numbers = [10, 20, 30, 40, 50]
let firstThree = numbers.prefix(3)

 

결과는

firstThree = [10, 20, 30]

즉, 배열의 앞에서부터 3개만 뽑은 새로운 배열이 생긴다.

 

이제 이걸 퀴즈앱에 적용해보자.

let quizList = [
    ("문제1", "O"),
    ("문제2", "X"),
    ("문제3", "O"),
    ("문제4", "X"),
    ("문제5", "X")
]

let selectedQuiz = quizList.shuffled().prefix(3)

shuffled()로 인해서 문제가 섞이고 그중 3개만 뽑히게 된다.

 

prefix()함수에 대해서 조사를 한 결과, prefix()의 결과는 ArraySlice라는 특별한 타입이라고 한다. 보통 배열처럼 쓸 수 있지만, 완전히 똑같은 배열은 아니라고 한다. 이게 무슨 의미일까?

 

이를 이해하기 위해서 아래 예시를 참고하도록 하자.

let numbers = [1, 2, 3, 4, 5]
let firstThree = numbers.prefix(3)
print(firstThree) // [1, 2, 3]

Swift에서 prefix(_ maxLength: Int) method는 배열(Array)의 앞에서부터 원하는 개수만큼의 요소를 잘라내는 것이라 하였다.

여기서 numbers는 배열(Array)이 이고 firstThree는 Array가 아니라 ArraySlice<Int> 이다. Array의 조각이라 생각하면 쉽게 이해될 듯 하다.

 

ArraySlice란?
ArraySlice는 이름 그대로 Array의 일부분(슬라이스) 을 나타내는 타입이다. Swift에서 성능 최적화를 위해 도입된 타입이라고 한다.

특징:
원본 배열을 복사하지 않고 그 일부를 참조한다 → 메모리 효율적이라고 한다.
인덱스가 0부터 다시 시작하지 않고, 원본 배열의 인덱스를 유지한다.

 

예를 들어보자.

let numbers = [10, 20, 30, 40, 50]
let slice = numbers.prefix(3) // [10, 20, 30]
print(slice.startIndex) // 0
print(slice.endIndex)   // 3

앞에서 3개를 가져왔기에 startIndex는 0이되고 endIndex가 3이다. 하지만 아래와 같이 중간을 슬라이스하면 어떻게 될까?

 

let middleSlice = numbers[1...3]
print(middleSlice) // [20, 30, 40]
print(middleSlice.startIndex) // 1

즉, 슬라이스는 원래 배열의 인덱스를 그대로 가지고 있음에 주의해야 한다!

 

그럼 왜 prefix()가 Array가 아니라 ArraySlice를 반환할까? 이유는 성능 때문이라고 한다.

  • Array는 전체 데이터를 새로 복사하므로 메모리 비용이 크다고 한다.
  • 반면 ArraySlice는 원본 데이터를 복사하지 않고 부분만 참조하기 때문에 훨씬 가볍고 빠르다고 한다.

따라서, prefix()처럼 부분만 필요할 때는 ArraySlice가 효율적이라고 한다.

 

만약 ArraySlice를 Array로 바꾸고 싶다면 아래와 같이 변환해서 사용하면 되겠다.

let array = Array(slice)

 

Array와 ArraySlice를 정리 요약하자면,

구분 Array ArraySlice
목적 일반 배열 배열의 일부분을 슬라이스
메모리 전체 복사 참조 (효율적)
인덱스 0부터 시작 원본 인덱스를 유지
사용 예시 전체 데이터 작업 일부 데이터 추출, 성능 최적화

 

위 내용을 적용하여 아래와 같이 코드를 업데이트를 했다.

var goodAnswer = 0
var wrongAnswer = 0
var answerPercentage = 0.0

print("총 다섯개 퀴즈 중 몇개를 풀고 싶으세요?:")
if let input = readLine(), let numQuiz = Int(input) {
    print("퀴즈 \(numQuiz)를 랜덤하게 드리겠습니다!")
    
    func runQuiz(question: String, correctAnswer: String) {
        print("\(question) (O/X)")
        if let answer = readLine() {
            if answer.uppercased() == correctAnswer.uppercased() {
                print("답변: \(answer)")
                print(">>>> 정답입니다!")
                goodAnswer += 1
            } else {
                print(">>>> 오답입니다!")
                wrongAnswer += 1
            }
        }
        print("정답: \(goodAnswer)개 / 오답: \(wrongAnswer)개")
    }
    
    let quizList = [
        ("대한민국의 수도는 서울이다.", "O"),
        ("대전은 대한민국의 도시 중 하나이다.", "O"),
        ("2024년도 12월 3일 발동한 비상계엄은 불법이다.", "O"),
        ("이스탄불은 대한민국 도시 중 하나이다.", "X"),
        ("챗GPT는 바보다.", "X")
    ]
    
    let shuffledQuizList = Array(quizList.shuffled().prefix(numQuiz))
    
    for (question, correctAnswer) in shuffledQuizList {
        runQuiz(question: question, correctAnswer: correctAnswer)
    }

    answerPercentage = Double(goodAnswer) / Double(goodAnswer + wrongAnswer) * 100
    print("===. 퀴즈가 종료되었습니다!  ===")
    print("총 정답: \(goodAnswer)개")
    print("총 오답: \(wrongAnswer)개")
    //print("정답율: \(answerPercentage)%")
    print(String(format: "정답율: %.1f%%", answerPercentage))
}

 

추가한 내용은

....

print("총 다섯개 퀴즈 중 몇개를 풀고 싶으세요?:")
if let input = readLine(), let numQuiz = Int(input) {
    print("퀴즈 \(numQuiz)를 랜덤하게 드리겠습니다!")
....

Array(quizList.shuffled().prefix(numQuiz))
....
}​

위와 같이 한 이유는 랜덤하게 문제를 섞고 그중에서 풀어볼 문제의 개수를 내가 정하고 싶어서 시작하게 되었다.

 

프로그램을 실행하면 "총 다섯개 퀴즈 중 몇개를 풀고 싶으세요?:"가 출력이되고 1...5숫자를 입력하면 출제되는 퀴즈의 개수가 정해진다. 다만 문제는 1...5외의 숫자나 String이 input되었을 때의 else가 없어 프로그램이 강제종료되는 문제가 있다.

 

이는 수정이 필요한 부분이다. 또 개선 할 수 있는 부분이 있는데 현재는 runQuiz() 함수가 if let input 블록 안에 정의돼 있는데, Swift에서는 보통 함수는 파일 맨 위쪽이나 main 바깥에서 선언하는 게 좋다고 한다. 이유는 나중에 알아가겠지만 당장은 작동엔 문제 없다.

하지만 구조적으로 더 깔끔하게 보이기 위해서 아래처럼 바꿔도 될 것 같다.

// runQuiz 함수는 맨 위로 빼기
func runQuiz(...) { ... }

print("몇 문제 풀고 싶나요?")
...

 

하여 수정된 코드는 아래와 같다.

var goodAnswer = 0
var wrongAnswer = 0
var answerPercentage = 0.0
var numQuiz = 0

    func runQuiz(question: String, correctAnswer: String) {
        print("\(question) (O/X)")
        if let answer = readLine() {
            if answer.uppercased() == correctAnswer.uppercased() {
                print("답변: \(answer)")
                print(">>>> 정답입니다!")
                goodAnswer += 1
            } else {
                print(">>>> 오답입니다!")
                wrongAnswer += 1
            }
        }
        print("\n정답: \(goodAnswer)개 / 오답: \(wrongAnswer)개\n")
    }

    print("총 다섯개 퀴즈 중 몇개를 풀고 싶으세요?:")
        
    if let input = readLine(), let inputNum = Int(input) {
        numQuiz = inputNum
        print("퀴즈 \(numQuiz)를 랜덤하게 드리겠습니다!")
    
    let quizList = [
        ("대한민국의 수도는 서울이다.", "O"),
        ("대전은 대한민국의 도시 중 하나이다.", "O"),
        ("2024년도 12월 3일 발동한 비상계엄은 불법이다.", "O"),
        ("이스탄불은 대한민국 도시 중 하나이다.", "X"),
        ("챗GPT는 바보다.", "X")
    ]
    
    let shuffledQuizList = Array(quizList.shuffled().prefix(numQuiz))
    
    for (question, correctAnswer) in shuffledQuizList {
        runQuiz(question: question, correctAnswer: correctAnswer)
    }
    
    answerPercentage = Double(goodAnswer) / Double(goodAnswer + wrongAnswer) * 100
    print("===. 퀴즈가 종료되었습니다!  ===")
    print("총 정답: \(goodAnswer)개")
    print("총 오답: \(wrongAnswer)개")
    //print("정답율: \(answerPercentage)%")
    print(String(format: "\n정답율: %.1f%%\n", answerPercentage))
}

 

다음 어떤 기능을 넣어볼까 생각하다가 오답이었던 문제들을 기억했다가 마지막에 복습차원에서 정답과 함께 모두 출력해보면 어떨까 하는 생각이 들어서 바로 적용을 해 보았다.

 

8. 오답 복습 기능 만들기 append()

이번에 구현하려고 하는 기능은 오답이 발생했을 때 이를 따로 저장하고, 퀴즈가 모두 끝난 뒤에 틀린 문제를 출력해주는 기능이다.

구현 방법은 오답을 저장할 배열을 만들어주고, 내가 오답을 낼 때 그 문제를 배열에 추가하고 퀴즈가 끝난 뒤 오답 배열을 반복문으로 출력하는 것이다.

 

저장용 배열은 아래 예시 코드를 참조하길 바란다.

var wrongQuestions: [(String, String)] = []

앞서 배운 튜플 배열로 (question, answer) 형식으로 저장을 할 것이다.

 

다음으로 오답일 경우 저장하는 기능을 적용할 것인데 이는 runQuiz 함수를 수정하면 가능하다.

func runQuiz(question: String, correctAnswer: String) {
    print("\(question) (O/X)")
    if let answer = readLine() {
        if answer.uppercased() == correctAnswer.uppercased() {
            print("정답입니다!")
            goodAnswer += 1
        } else {
            print("오답입니다!")
            wrongAnswer += 1
            wrongQuestions.append((question, correctAnswer)) // ⬅️ 오답 저장
        }
    }
}

 

그리고 마지막으로 퀴즈 종료 후 오답 복습 출력을 할 코드를 적용해보자.

if !wrongQuestions.isEmpty {
    print("\n💡 당신이 틀린 문제를 복습해볼까요?\n")
    for (question, correctAnswer) in wrongQuestions {
        print("문제: \(question)")
        print("정답: \(correctAnswer)")
        print("----------------------------")
    }
} else {
    print("\n🎉 모든 문제를 맞혔어요! 복습할 것이 없습니다!")
}

 

 최종 코드는 아래와 같다.

var goodAnswer = 0
var wrongAnswer = 0
var wrongQuestions: [(String, String)] = []
var answerPercentage = 0.0
var numQuiz = 0

    func runQuiz(question: String, correctAnswer: String) {
        print("\(question) (O/X)")
        if let answer = readLine() {
            if answer.uppercased() == correctAnswer.uppercased() {
                print("답변: \(answer)")
                print(">>>> 정답입니다!")
                goodAnswer += 1
            } else {
                print(">>>> 오답입니다!")
                wrongAnswer += 1
                wrongQuestions.append((question, correctAnswer))
            }
        }
        print("\n정답: \(goodAnswer)개 / 오답: \(wrongAnswer)개\n")
    }

    print("총 다섯개 퀴즈 중 몇개를 풀고 싶으세요?:")
        
    if let input = readLine(), let inputNum = Int(input) {
        numQuiz = inputNum
        print("퀴즈 \(numQuiz)를 랜덤하게 드리겠습니다!")
    
    let quizList = [
        ("대한민국의 수도는 서울이다.", "O"),
        ("대전은 대한민국의 도시 중 하나이다.", "O"),
        ("2024년도 12월 3일 발동한 비상계엄은 불법이다.", "O"),
        ("이스탄불은 대한민국 도시 중 하나이다.", "X"),
        ("챗GPT는 바보다.", "X")
    ]
    
    let shuffledQuizList = Array(quizList.shuffled().prefix(numQuiz))
    
    for (question, correctAnswer) in shuffledQuizList {
        runQuiz(question: question, correctAnswer: correctAnswer)
    }
    
    answerPercentage = Double(goodAnswer) / Double(goodAnswer + wrongAnswer) * 100
    print("===. 퀴즈가 종료되었습니다!  ===")
    print("총 정답: \(goodAnswer)개")
    print("총 오답: \(wrongAnswer)개")
    //print("정답율: \(answerPercentage)%")
    print(String(format: "\n정답율: %.1f%%\n", answerPercentage))

    if !wrongQuestions.isEmpty {
    print("\n오답 질문들을 복습해볼까요?\n")
        for (question, correctAnswer) in wrongQuestions {
        print("문제: \(question)")
        print("정답: \(correctAnswer)")
        print("-----------------------------------")
    }
        } else {
            print("모든 문제를 맞추셨습니다! 복습이 필요없어요!")
    }
}

 

계속하다보니 코드가 꽤 길어진 것 같다. 초보자로써 상당히 뿌듯하다.

이 프로그램이 앞으로 어떻게 발전될지 상당히 궁금하다.

 

오늘은 포스팅을 여기까지하고 앞으로의 기능을 계속 추가할 때마다 그리고 프로그래밍하는 과정에서 발생하는 문제들을 어떻게 해결했는지 등을 상세히 적도록 하겠다. 포스팅하면서 복습을하게되니 더더욱이 기억에 남고 확실한 공부가 되는 것 같다.

 

배움에 나이가 없다는 것을 다시 한번 깨달았다.

정진하자!

 

728x90
반응형