iOS 앱 개발 부트캠프/TIL

TIL 9일차 - 성적 관리 시스템2

iosstudyletsgo 2024. 9. 24. 20:23

어제에 이어 성적 관리 시스템을 마저 프로그래밍 했다.

어제 구조체를 쓰긴 했는데 여러 함수들을 구조체 밖에다 만든 것이 마음에 안들었다. 구조체는 특정 기능과 관련된 변수나 함수들을 한데 모아 관리할 수 있어 명확하고 변경이나 추가, 삭제가 편리한 것으로 이해하고 있기 때문이다.

다음과 같이 학생들의 이름을 저장할 배열 studentList와 이름과 아이디를 저장할 딕셔너리 studentDictionary, 학생의 이름과 아이디를 입력 받아 배열과 딕셔너리에 추가할 함수 addStudent, 추가된 것을 확인할 출력 함수 printStudents를 구조체 안에 추가하였다.

구조체 안에 추가하였으므로 studentList를 선언할 때 썼던 타입을 Student에서 String으로 바꾸었고, addStudent 함수 안의 let inputNewStuendt = Student(~) 부분이 Student 인스턴스를 생성하는 부분이니 삭제하였고, printStudent 함수 안에서도 student 인스턴스를 활용하는 부분을 수정하였다.

메인 부분에서도 구조체의 인스턴스를 studentTest로 하여 임시로 만들었고, UI로부터 입력 받은 값들을 이 인스턴스에 addStudnet 함수를 호출해 명단을 추가시키고, studentTest.printSudents를 통해 출력해 확인하고자 하였다.

이와 같이 작성하니 구조체 안에 addStudent 함수에서 배열과 딕셔너리에 값을 추가하는 studentList.append와 studentDictionary[id]=name 부분에 에러가 났고, 메인 부분에서도 인스턴스를 생성하는 var studentTest = Student() 부분에서 에러가 났다.

addStudent 함수에서 에러가 나는 건 구조체 내 함수인 addStudent 안에서 사용되는 변수 studentList와 studentDictionary가 인스턴스여서 그렇다. 복사본인 인스턴스를 쓰는 건데 그걸 가지고 원본인 변수의 값을 변경하려고 하니 에러가 난 것이다. 이를 해결하려면 mutating func이라는 다른 함수를 써야한다고 하는데

내가 이해한 것을 토대로 비유하자면 예를 들어 중요한 내용을 메모 해놓고(변수 선언), 그 메모를 복사해 복사한 메모 파일(인스턴스)만 편집 도구(함수)를 통해 수정하거나 다시 복사하거나 하는 건데 이 편집 도구로 원본 메모를 수정하려고 하니 허용되지 않아 에러가 나는 느낌인 것 같다. 이 원본 메모를 수정하려면 다른 특별한 편집 도구(mutating func)를 써야하는데 이렇게 되면 복사본들의 내용도 다같이 변경되니 주의해야 하는 것 같다.

마지막으로 메인 부분의 var studentTest = Student()에서 나는 에러는 Student의 변수들의 기본값이 지정되어야 하는데 없어서 그렇다고 한다. 구조체의 인스턴스를 생성할 때 모든 변수의 값이 지정되어야 하는데 그런 거 없이 Student() 형태로 선언해 생성하였으니 구조체 내에 기본 값이 있어야 한다.

이 경우에 매개변수로 전달되어야 하는 studentName과 studentId의 값이 지정되어 있지 않아서 나는 에러인 것이다. 따라서 구조체 내부에 이니셜라이저를 추가해 studentName과 studentId의 기본 값을 설정해야한다.

이렇게 다 작성하고 나서 테스트 해보니 에러는 없지만 출력하는 부분이 뭔가 이상해졌다. 확인해보니 반복문에서 단순하게 변수 몇개만 바꿨더니 이름을 저장한 배열을 출력하는 부분의 반복문의 studentName과 studentId가 입력한 값을 불러오는 것이 아닌 그냥 이니셜라이저의 초기값인 공백을 불러오고, student는 붕떠서 아무 역할도 하지 않고 있다.

이런 저런 해결법을 찾다가 보니 구조체에 대한 내용을 찾았는데, 구조체가 아니라 클래스(Class)를 쓰는 방법도 있었다. 처음에 mutating func 형태로 변수와 인스턴스 값 변경에 대한 내용으로 조금 복잡하게 느꼈는데 처음부터 구조체인 Struct이 아닌 Class를 썼다면 func 형태로 사용 가능한 것인듯 했다.

이후 나머지 문제도 많이 남았으니 이어서 풀어보고자 했는데, 과목들을 Set형태로 저장해 추가, 삭제를 하는 문제와 과목별 성적을 Array형태로 받아 추가, 삭제, 수정을 하는 문제 그리고 성적들의 평균을 계산하는 함수 구현 문제가 있었다.

이것들을 입력 받아 처리해서 보이려니 너무 어렵게 느껴졌지만 일단 차근차근 해보기로 했다. UI에서 입력 받고 출력하는 건 텍스트필드나 네비게이션등을 써서 하면 될 것 같았는데 일단 나중에 하고 코드 구현부터 하기로 했다.

struct Student{
    let studentName : String
    let studentId : String
}

class StudentManager {
    var studentList : [Student] = []
    var studentDictionary : [String : String] = [:]
    var subjectName : Set<String> = []
    var grade : [String : [Int]] = [:] //과목 이름을 키로 하고, 밸류인 성적을 배열로 저장하는 딕셔너리
    
    func printStudents() {
        print("\n--- 학생 ID와 이름 ---")
        for (id, name) in studentDictionary {
            print("ID: \(id), 이름: \(name)")
        }
    }

학생 이름과 아이디는 구조체 Student에서 선언해 관리하고, 함수 구현에 필요한 변수들은 StudentManager 클래스에서 관리하기 위해 따로 선언하였다. 초반에 전부 Struct안에 구현하다가 든 생각이, 이름과 아이디를 관리하는 구조체 안에 추가, 삭제, 수정에 관한 함수가 꼭 한 번에 같이 있을 필요는 없고 오히려 따로 관리되어야 유지보수가 편리할 것 같다는 생각이 들어서였다.

    func addStudent(name : String, id : String){ // 학생 이름과 아이디 추가
        studentList.append(Student(studentName: name, studentId: id))
        studentDictionary[id] = name
    }

처음은 학생의 이름과 아이디를 추가하는 addStudent 함수이다. studentList에 append를 호출해 배열에 Student 구조체의 이름과 아이디를 추가해 배열로 넣고, 딕셔너리엔 id를 키로 하여 name을 추가했다. 이건 어제도 하던거라 어렵지 않았다.

  func addSubject(subject : String){  //과목 추가
        subjectName.insert(subject)
    }
    func removeSubject(subject : String){   //과목 삭제 및 해당 과목의 성적 동시 삭제
        subjectName.remove(subject)
        grade.removeValue(forKey: subject)
    }

다음은 과목의 추가, 삭제 부분이다. Set 타입인데 배열에서 append를 쓰는 것과 크게 다르지 않게 insert로 추가하고 remove로 삭제한다.

다음으로 성적을 Array로 받고 성적이 추가, 삭제, 수정 기능을 구현하는 부분이다. 이 부분과 과목을 저장하는 Set 부분을 고려하여 UI를 통해 입력을 받으려니 굉장히 오래걸렸다.

여러 학생의 이름과 아이디를 입력 받은 뒤, 과목들을 입력하고 삭제하는 화면을 만들고, 성적을 입력 받아 나중에 평균까지 내는 화면을 만들으려고 하니 뭐부터 해야하는지 고민이었다. 과목을 성적과 따로 입력받으면 과목 따로 Set에 저장하고 나중에 입력 받은 성적은 Array에 저장하고 나면, 누구의 어떤 과목이 몇 점인지 어떻게 처리해야 하지? 하는 고민이었다.

고민 끝에 생각해낸 방법은 점수를 관리할 grade 변수를 선언해 과목 이름을 key로, value인 성적을 배열로 저장하는 딕셔너리를 만드는 것이었다. 과목명을 가지고 성적을 다루기로 한 것인데 성적을 굳이 배열로 한 것은 과목별로 여러 학생의 점수를 받고싶어서였다. (TIL 작성하면서 다시보니 학생별 점수인지 구분하는게 안 되어서 코드를 추가해야 할 것 같긴하다.)

    func addGrade(subject : String, score : Int){
        if grade[subject] == nil {  //딕셔너리에서 키 값(subject)이 nil인지 확인하여 성적이 추가되지 않았는지 체크
            grade[subject] = []     //만약 과목의 성적이 없다면 빈 배열로 초기화하여 성적을 저장할 준비, 딕셔너리에 새로운 과목 추가
        }
        grade[subject]?.append(score)   //grade[subject]가 존재할 때 성적 추가
    }
    func removeGrade(subject : String, score : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return } //딕셔너리 grade에서 subject라는 키에 대한 배열에서 score의 인덱스 찾기, firstIndex(of:score)->배열에서 첫번째로 일치하는 성적의 인덱스 반환 없으면 nil반환
        grade[subject]?.remove(at: scoreIndex)  //scoreIndex가 유효한 경우 = 해당 성적이 존재하는 경우 해당 인덱스의 성적을 배열에서 제거
    }
    func updateGrade(subject : String, score : Int, newScore : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return }
        grade[subject]?.remove(at: scoreIndex)  //removeGrade와 같은 방식으로 기존 성적 삭제
        grade[subject]?.append(newScore)        //삭제된 위치에 새로운 성적 추가
    }
}

addGrade 함수를 선언하고 if grade[subject]==nil 형태로 딕셔너리의 키 값이 nil인지 확인한다. 단순하게 아무것도 없이 함수 안에 grade[subject].append(score)만을 사용하면 nil상태의 배열에 값을 추가하게 되어 하면 에러가 발생한다. 따라서 if문을 통해 배열을 초기화 해야한다.

grade 변수의 키 값은 과목명이므로 해당 과목명을 키로 하는 값이 nil이라면 해당 과목이 저장되지 않은 것이다. 따라서 grade[subject]=[]의 형태로 해당 과목을 키로 갖는 빈 배열을 추가한다.

이렇게 하면 예를 들어 grade에 수학 50점,100점, 영어 60점, 90점이 추가되어 있는 상태에서 국어 점수를 추가하고자 하는 상황이라면, grade는

grade: [String: [Int]] = [”수학”: [50,100], “영어”: [60,90]]

형태로 있으니 국어는 없어서 if문을 수행하게 된다. 그러면 grade는

grade: [String: [Int]] = [”수학”: [50,100], “영어”: [60,90], “국어”: []]

형태로 빈 배열이 추가되어 국어 성적을 입력받을 준비가 되는 것이다. 그리고 과목명이 존재하는 경우엔 append(score)를 통해 해당 과목의 성적이 배열에 입력될 것이다.

다음은 removeGrade를 선언해 성적을 삭제하는 함수를 작성했다. 과목명과 점수를 입력 받아서 해당 과목의 특정 점수를 삭제해야 하므로 grade에서 과목명을 키로 하여 해당 과목의 성적 배열을 가지고 온다. 그 뒤 입력 받은 점수와 같은 점수를 찾아 삭제해야 하므로

grade[subject]?.firstIndex(of: score)

의 형태로 코드를 썼다. firstIndex(of: )가 가져온 배열에서 of 뒤에 오는 요소와 일치하는 첫번째 요소의 인덱스를 찾는 것이라고 한다. 이렇게 찾은 인덱스를 scoreIndex에 저장하고 그 값이 nil이 아닌 유효한 값이라면 해당 인덱스의 값을 remove를 수행해 삭제한다. 만약 해당하는 점수가 없다면 else {return}이 동작해 아무것도 하지않고 함수가 종료된다.

마자막으로 성적을 수정하는 updateGrade를 작성했다. 기본적으로 삭제와 거의 같지만 새로 성적을 추가하는 부분이 있어야 하기에 새로 등록할 점수를 따로 더 입력받은 뒤 삭제와 같은 방식으로 처리한 다음 마지막에

grade[subject]?.append(newScore)

를 통해 점수를 수정하도록 하였다.

struct Student{
    let studentName : String
    let studentId : String
}

class StudentManager {
    var studentList : [Student] = []
    var studentDictionary : [String : String] = [:]
    var subjectName : Set<String> = []
    var grade : [String : [Int]] = [:] //과목 이름을 키로 하고, 밸류인 성적을 배열로 저장하는 딕셔너리
    
    func printStudents() {
        print("\n--- 학생 ID와 이름 ---")
        for (id, name) in studentDictionary {
            print("ID: \(id), 이름: \(name)")
        }
    }
    
    func addStudent(name : String, id : String){ // 학생 이름과 아이디 추가
        studentList.append(Student(studentName: name, studentId: id))
        studentDictionary[id] = name
    }
    
    func addSubject(subject : String){  //과목 추가
        subjectName.insert(subject)
    }
    func removeSubject(subject : String){   //과목 삭제 및 해당 과목의 성적 동시 삭제
        subjectName.remove(subject)
        grade.removeValue(forKey: subject)
    }
    
    func addGrade(subject : String, score : Int){
        if grade[subject] == nil {  //딕셔너리에서 키 값(subject)이 nil인지 확인하여 성적이 추가되지 않았는지 체크
            grade[subject] = []     //만약 과목의 성적이 없다면 빈 배열로 초기화하여 성적을 저장할 준비, 딕셔너리에 새로운 과목 추가
        }
        grade[subject]?.append(score)   //grade[subject]가 존재할 때 성적 추가
    }
    func removeGrade(subject : String, score : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return } //딕셔너리 grade에서 subject라는 키에 대한 배열에서 score의 인덱스 찾기, firstIndex(of:score)->배열에서 첫번째로 일치하는 성적의 인덱스 반환 없으면 nil반환
        grade[subject]?.remove(at: scoreIndex)  //scoreIndex가 유효한 경우 = 해당 성적이 존재하는 경우 해당 인덱스의 성적을 배열에서 제거
    }
    func updateGrade(subject : String, score : Int, newScore : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return }
        grade[subject]?.remove(at: scoreIndex)  //removeGrade와 같은 방식으로 기존 성적 삭제
        grade[subject]?.append(newScore)        //삭제된 위치에 새로운 성적 추가
    }
}

class ViewController: UIViewController {

    @IBOutlet weak var nameField: UITextField! //이름 입력하는 노란색 textField
    @IBOutlet weak var idField: UITextField!    //id 입력하는 민트색 textField
    @IBOutlet weak var returnButton: UIButton! //입력한 내용 제출하는 버튼
    
    var studentManager = StudentManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        returnButton.layer.cornerRadius = 10
    }
    
    @IBAction func TabButton(_ sender: Any) {
        guard let inputName = nameField.text, !inputName.isEmpty, // 입력된 이름이 비어있지 않을 시 inputName에 내용 저장
              let inputId = idField.text, !inputId.isEmpty        // 입력된 아이디가 비어있지 않을 시 inputId에 내용 저장
        else {
            print("이름과 id를 입력하세요")
            return
        }
        
        studentManager.addStudent(name: inputName, id: inputId)
        studentManager.printStudents()
    }
}

전체 내용은 이렇게 작성하였다. 아직 못다한 부분이 성적의 평균을 내는 부분과, 학생별 성적을 구분하는 코드, 그리고 UI로 화면 구성을 해서 학생 이름과 아이디를 입력 받고, 새로운 화면을 띄워 과목명과 성적을 입력하게 하고, 추가/수정/삭제/평균을 내는 버튼을 만들어 다른 화면과 연결하려고 한다. 아마 내가 배운 내용 중 네비게이션 컨트롤러를 쓰면 가능할 것 같은데 이것도 또 해보면 에러가 많이나 시간이 많이 걸릴 수도 있다..

'iOS 앱 개발 부트캠프 > TIL' 카테고리의 다른 글

TIL 11일차  (1) 2024.09.26
TIL 10일차  (1) 2024.09.25
TIL 8일차 - 성적 관리 시스템 제작하기  (0) 2024.09.23
TIL 7일차 - 간단한 데이터 타입 연습  (0) 2024.09.20
TIL 6일차 - 피그마와 친해지기  (1) 2024.09.19