iOS 공부하는 감자

[TripCard] 출시 프로젝트 회고 본문

TripCard

[TripCard] 출시 프로젝트 회고

DongTaTo 2022. 10. 5. 15:29
반응형

https://apps.apple.com/kr/app/tripcard/id1645004488

 

앱 정보와 기획 의도

나는 여행을 엄청 좋아한다.

군대에 있을 땐 월급을 전부 적금에 넣으면서 1년 가까이 유럽 여행을 준비했고, 사적 국외여행 신청서를 따로 제출하고 휴가 써서 일본을 혼자 다녀올 정도로 좋아했다.

근데 전역할 때쯤 코로나가 터지더니 유럽여행은 공중분해되고 그 뒤로 이런저런 이유로 여행을 많이 가지 못했다..

 

공부하느라 바쁜 와중에도 여행 생각은 틈틈이 나서(특히 자기 전에) 가끔 갤러리에 들어가서 사진을 보곤 하는데

여행 관련 사진만 골라서 보고 싶은디 사진이 하도 많으니까 그게 어려웠다. (스크롤 압박..)

 

SlideBox라는 사진을 정리해주는 어플을 써서 정리해보려 했는데.. 이게 한 장 한 장 노다가로 카테고리를 구분해주는 방식이라 너무 오래 걸려서 1/4쯤 정리하다가 관뒀다.

 

그래서.. 내가 쓰려고 여행을 기록하는 앱을 만들기로 했다. (여행스타그램을 비공개로 만들어도 되지만, 그래도 직접 만드는 게 또 의미가 있으니깐?)

 

일단 난 복잡한걸 싫어하고, 일기를 쓰거나.. 다이어리를 쓰는(?) 활동을 상당히 귀찮아한다..

그래서 기획할 때 앱의 특징을 다음과 같이 잡았다.

  1. UI는 직관적이고, 복잡하지 않도록 만들기 (앱을 처음 사용하는 사람도 쉽게 사용할 수 있도록)
  2. 여행을 기록하는 과정이 귀찮지 않도록 최소한의 데이터?로 기록하도록 만들기
  3. 기록된 내용을 한눈에 보기 쉽도록 만들기

 

그렇게 여행 기록을 카드로 만들어서 저장한다는 컨셉을 바탕으로 앱을 기획했다.

메인화면에서는 생성된 여행 카드를 그리드 형식으로 한눈에 보기 쉽도록 만들고, 각 카드를 조회하면 좌우로 스와이핑 가능하도록 기획했다. (카톡의 업데이트한 친구 리스트를 스와이핑 하면서 넘겨보는 것처럼!)

 

사진은 다음과 같은 이유 때문에 의도적으로 한 장만 저장하도록 기획했다.

  1. 여행 가면 찍는 사진은 엄-청 많은데, 다 등록하기에는 용량 압박의 문제가 있다. (내 앱은 로컬 저장공간에 사진을 저장하기 때문)
  2. 여행 기록을 카드처럼 슉슉 넘기는 형태로 조회하도록 기획했는데, 사진을 여러 장 등록하면 한 화면에서 2중으로 스크롤이 가능하기 때문에 사용자가 불편함을 느낄 수 있다.

 

 

 

코드 컨벤션

어차피 혼자 작업하는 프로젝트긴 하지만, n개월 후의 나를 위해서라도(?) 코드를 규칙적으로 작성하려고 노력했다.

  1. 예약어는 사용하지 않는다.
  2. import는 알파벳 순서로 정리하고, 프레임워크와 라이브러리를 구분한다.
  3. 프로퍼티는 1칸, 메서드는 2칸의 줄 간격을 기본으로 한다.
    1. 클로저를 활용한 프로퍼티의 초기화가 많이 필요한 부분에서는 가독성을 위해 2칸의 간격을 띄움
    2. 데이터 로직을 처리하는 메서드들은 상대적으로 라인이 길어서 3칸의 간격을 띄움
  4. 프로퍼티는 명사, 메서드는 동사를 사용하는 것을 기본으로 한다.
  5. 프로퍼티와 메서드는 이름이 길어지더라도 명확하게 역할을 알 수 있도록 작성한다.
  6. // MARK 를 사용하여 프로퍼티, 이니셜라이저, 메서드를 구분하여 작성하고, 각각 4칸의 간격을 띄운다.
  7. 프로토콜 채택은 Extension을 활용하여 코드를 분리하고, 4칸을 띄워서 작성한다.
  8. 메서드의 라인이 15줄을 넘기지 않도록 최대한 분리한다. (정리가 조금 더 필요함)
  9. 더 이상 상속이 필요하지 않은 클래스는 final 키워드를 붙인다.
  10. 외부에서 사용되지 않는 프로퍼티, 메서드는 private 키워드를 붙인다.
  11. 필수로 필요한 프로퍼티가 아니라면 lazy를 활용한다. (특히 클로저 구문으로 생성하는 클래스의 인스턴스 등?)

 

 

 

UI / UX

미적 감각이 0에 수렴하는 인간이라 디자인하는 과정이 정말 고통스러웠다.

코드도 UI도 깔끔하게 만드는 개발자가 되고 싶다고 항상 생각했었는데, 코드는 어떻게든 하겠어도 UI는 진짜 안 되겠다는 걸 느꼈다.

 

기능이 많지는 않았으니 디자인이라도 최대한 이쁘게 만들어보려구 폰트 변경도 넣고, 테마 색상도 여러 개 추가해서 변경 가능하도록 만들었는데 그렇게 많은 시간을 들여서 열심히 만들었던(?) 초기 버전은 디자인이 어딘가 구렸다.. 딱 보면 좀 이상한데, 어디를 고쳐야 할지 감을 못 잡았었다. 다행히도 멘토님과 팀원분들이 조금씩 문제점을 찾아주셔서 많이 개선되었고 출시하는 시점에는 나름 만족스러운 디자인으로 출시할 수 있었다.

 

디자인 관련해서 주워들은 내용들은 좀 있어서 아래의 내용들은 프로젝트에 적용했다.

 

1. 글자는 최대한 적게 사용하기

설명이 없어도 본능적으로(?) 사용할 수 있는 디자인이 좋은 디자인.. 이라고 어디선가 봤다.

글자가 많아지면 화면이 난잡해진다.

 

 

2. PlaceHolder는 글자가 입력돼도 확인할 수 있도록 만들기

글자가 입력되어 있어도 해당 입력창이 무엇을 요구하고 있었는지 확인이 가능해야 한다.

 

 

3. Alert는 설명 텍스트를 읽지 않고 버튼의 글자만으로 사용자가 선택할 수 있도록 만들기

아래 사진처럼 Alert가 올라왔을 때, 설명 텍스트를 읽지 않아도 명확하게 각 버튼이 무엇을 의미하는지 써주는 게 좋다.. (고 봤다)

 

 

 

Database

Realm에는 국내/국외 여행 구분, 지역, 기간, 여행 날짜별로 작성된 문자열을 저장했다.

enum TripType: Int {
    case domestic
    case overseas
}


typealias TripPeriod = (start: Date, end: Date)


final class Trip: Object, Codable {
    @Persisted var isDomestic: Bool
    @Persisted var location: String
    @Persisted var startDate: Date
    @Persisted var endDate: Date
    @Persisted var numberOfDate: Int

    @Persisted var contentByDate: List<String?>
    
    @Persisted(primaryKey: true) var objectId: ObjectId
    
    
    convenience init(tripType: TripType, location: String, tripPeriod: TripPeriod, contentByDate: List<String?>) {
        self.init()
        
        self.isDomestic = tripType == .domestic
        self.location = location
        self.startDate = tripPeriod.start
        self.endDate = tripPeriod.end
        self.numberOfDate = Date.calcDateDifference(startDate: startDate, endDate: endDate)
        
        self.contentByDate = contentByDate
    }
    
    
    enum CodingKeys: String, CodingKey {
        case isDomestic
        case location
        case startDate
        case endDate
        case numberOfDate
        case contentByDate
        case objectId
    }
    
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(objectId, forKey: .objectId)
        try container.encode(isDomestic, forKey: .isDomestic)
        try container.encode(location, forKey: .location)
        try container.encode(startDate, forKey: .startDate)
        try container.encode(endDate, forKey: .endDate)
        try container.encode(numberOfDate, forKey: .numberOfDate)
        try container.encode(contentByDate, forKey: .contentByDate)
    }
}

 

 

이미지는 Document 디렉터리에서 Images 폴더를 생성한 후, 

ObjectID를 기준으로 여행 기록별 디렉터리를 추가로 생성해서 이미지를 저장했다.

Document > Images > 6332e9a846dc369d8e4a85cc 이런 식으로..

이렇게 PK값으로 폴더를 따로 생성하고 이미지를 관리하니까 수정이나 삭제를 할 때, 디렉터리 단위로 데이터를 처리할 수 있어서 좀 편했던 것 같다.

 

 

데이터를 관리하는 부분은 Realm을 포함하여 데이터 전반을 관리하는 싱글톤 객체와 Document 폴더를 관리하는 인스턴스를 따로 두어서 코드를 분리했다.

내부적으론 싱글톤 객체에서 저장 프로퍼티로 DocumentManager를 가지고 사용하는 구조인데.. 이게 좋은 방법인지는 잘 모르겠다..

일단 앱을 사용하기 위해 두 인스턴스가 반드시 필요하기 때문에 각각 1개의 인스턴스만 생성되도록 만들었다.

 

 

 

 

 

FSCalendar 라이브러리 커스텀

항공권 어플처럼 시작일, 종료일을 선택하면 기간이 선택되도록 커스텀했다.

  1. 2개의 날짜를 사용자가 선택하면 두 날짜를 연결한다.
  2. 기간이 선택된 상태에서 새로운 날짜를 선택하면 먼저 설정되었던 기간은 사라진다.
  3. 선택된 날짜, 기간을 Label을 사용하여 표시

month, year를 변경하는 버튼이 따로 존재하지 않아서 커스텀 View를 만들어서 활용했다.

 

 

 

메모리 릭

개발을 진행하던 중, 시뮬레이터로 테스트를 할 때마다 사용하는 메모리 양이 매우 불규칙하다는 것을 확인했다.

 

어디선가 메모리 누수가 발생했음을 알게 되었고, deinit을 찍어가며 테스트한 결과 작성 화면에서 메모리가 해제되지 않음을 확인했다.

 

작성 화면은 전체가 TableView로 구성되어 있다.

이미지부터 기간을 입력하는 TextView까지는 Header로 들어가고

날짜별 사진과 텍스트를 입력하는 부분은 Cell로 구성했다.

 

구조가 이렇다 보니 Header의 뷰객체에 입력된 데이터를 ViewController로 전달하기 위해 Delegate Protocol을 생성하여 값 전달을 했는데, 문제는 delegate 프로퍼티를 강한 참조로 선언했다는 것..! 이었다.

var delegate: WritingDelegate?

 

 

delegate 프로퍼티가 프로토콜 타입으로 선언되긴 했지만, 본질적으로 ViewController의 인스턴스를 전달받기 때문에 뷰컨의 RC를 증가시켰고, 뷰컨과 Header가 서로를 강하게 참조하면서 순환 참조가 발생했었다.

 

습관적으로 delegate를 활용한 값 전달을 구현할 땐 weak를 사용해야겠다..

 

이번에 알게 된 건데 TableView, TextField에서 사용되는 delegate 프로퍼티도 모두 weak로 선언되어 있었다. (생각해보니 당연한..)

 

 

 

 

 

설정 화면의 각 Cell 정보를 열거형으로 선언

이번 프로젝트를 수행할 때, 열거형을 많이 사용했는데 이렇게 작성해두니 코드가 깔끔해져서 보기 좋았다.

enum SettingCellList: String {
    case changeFont = "change_font"
    case changeThemeColor = "change_theme_color"
    case changeImageQuality = "change_image_quality"
    case backUpAndRestore = "backup_and_restore"
    case reset = "reset"
    case bugReportAndFeedback = "bug_report_and_feedback"
    case appStoreReview = "app_store_review"
    case versionInfo = "version_info"
    case openSource = "opensource"
}

 

private let cellItems: [[SettingCellList]] = [
        [.changeFont, .changeThemeColor, .changeImageQuality],
        [.backUpAndRestore, .reset],
        [.bugReportAndFeedback, .appStoreReview],
        [.versionInfo, .openSource]
]

 

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedCell = cellItems[indexPath.section][indexPath.row]
        
        var selectedVC: UIViewController?
        
        switch selectedCell {
        case .changeFont: selectedVC = ChangeFontViewController()
        case .changeThemeColor: selectedVC = ChangeColorViewController()
        case .changeImageQuality: selectedVC = ChangeImageQualityViewController()
        case .backUpAndRestore: selectedVC = BackupRestoreViewController()
        case .reset: resetAppData()
        case .bugReportAndFeedback: sendEmail()
        case .appStoreReview: openAppStore()
        case .versionInfo: break
        case .openSource: selectedVC = OpenSourceListViewController()
        }
        
        if let selectedVC = selectedVC {
            transition(selectedVC, transitionStyle: .push)
        }
    }

 

 

 

Document에 저장한 이미지가 nil로 넘어오는 이슈

도큐먼트 디렉터리에는 이미지가 잘 저장되어 있는데도 파일을 가져올 때 nil이 넘어와서 당황했었는데

확장자를 명확하게 쓰지 않아서 발생했던 문제였다..!

// 메인 이미지
let result = {
		// 메인 이미지는 확장자 없이 일반 파일로 저장했었다.
    let fileURL = directoryPath.appendingPathComponent("mainImage")
    let imageData = mainImage.jpegData(compressionQuality: 0.3) ?? Data()
    
    return (fileURL: fileURL, imageData: imageData)
}()


// 날짜별 이미지
let resultByDate = imageByDate.enumerated().map { index, image in
    let fileURL = directoryPath.appendingPathComponent("day\(index + 1)Image.jpg")
    let imageData = image?.jpegData(compressionQuality: 0.3)
    
    let result = (fileURL: fileURL, imageData: imageData)
    
    return result
}
func loadMainImageFromDocument(directoryName: String) throws -> UIImage? {
    guard let imagesPath = imageDirectoryPath() else { throw DocumentError.fetchDirectoryPathError }
    
    let directoryURL = imagesPath.appendingPathComponent(directoryName, isDirectory: true)
    
    let mainImagePath = directoryURL.appendingPathComponent("mainImage")
    
    var image: UIImage?
    if #available(iOS 16, *) {
        image = UIImage(contentsOfFile: mainImagePath.path())
    }else {
        image = UIImage(contentsOfFile: mainImagePath.path)
    }
    
    return image
}


func loadImagesFromDocument(directoryName: String, numberOfTripDate: Int) throws -> [UIImage?] {
    guard let imagesPath = imageDirectoryPath() else { throw DocumentError.fetchDirectoryPathError }
    
    let directoryURL = imagesPath.appendingPathComponent(directoryName, isDirectory: true)

    let resultImages = (1...numberOfTripDate).map {
        let eachImageURL = directoryURL.appendingPathComponent("day\($0)Image")
        
        if #available(iOS 16, *) {
            return UIImage(contentsOfFile: eachImageURL.path())
        }else {
            return UIImage(contentsOfFile: eachImageURL.path)
        }
    }
    
    return resultImages
}

 

 

 

 

TextView Auto Height

여행 날짜별로 입력하는 위 화면에서는 TextView를 사용하여 짧은 일기를 작성하도록 구현했다.

구조는 ScrollView에 UIImageView와 TextView가 들어간 상태인데, 사용자가 줄 바꿈을 하는 등 TextView의 크기가 동적으로 변할 필요가 있을 때, 높이값을 늘려주고 싶었다.

(고정 높이값을 줘서 텍스트가 많아졌을 때, 2중으로 스크롤되는 형태로 만들고 싶지 않았음..!!)

 

프로젝트 초반에는 TextView의 Intrinsic Content Size를 사용하여 동적인 높이를 부여했는데, 그렇게 구현하니까 텍스트가 입력되지 않은 상태에서는 높이값이 너무 작았던 게 사용하기 좀 불편하게 느껴졌었다.

배경색도 따로 넣어서 ImageView, TextView의 경계를 명확하게 하고 싶었는데, 데이터가 입력되기 전에는 1칸짜리 높이만 부여돼서 디자인이 좀 엉성하다고 해야 되나.. 좀 별로였다..

 

그래서 min height를 부여하는 방법이 있으려나 찾아봤지만.. 마땅한 정보를 찾지 못해서 좋은 방법인지는 모르겠지만 UIView와 제스처를 활용하여 좀 개선을 했다.

 

TextView와 SafeArea 사이의 공백을 동일한 배경색과 Radius를 부여한 UIView로 채우고, 제스쳐를 추가해서 UIView가 Tap 되었을 때, TextView가 FirstResponder가 되도록 설정했다.

 

이렇게 만드니까 디자인은 내가 원하는 방식대로 만들어졌지만, 키보드를 올렸을 때 스크롤링?이 좀 자연스럽지 않은 문제가 있어서.. 이건 추후에 다시 수정이 필요할 것 같다.

 

 

 

 

 

CollectionView 구조, 레이아웃 삽질

내 앱은 여행 카드들을 메인화면에서 그리드 형식으로 보여주고, 카드를 선택하면 해당 카드를 보여주고, 좌우로 스와이핑 하면서 다른 카드를 조회할 수 있어야 한다.

아래 화면에서 도쿄 카드를 선택하면 크게 띄우고, 좌우로 스와이핑 가능해야 함!!

이게 가능하려면 카드 조회 화면을 띄울 때, scrollToItem 속성을 활용하여 사용자가 선택한 카드를 보여주도록 해야 한다.

그냥 인덱스로 해당 카드를 보여주면 좌우 스와이핑이 불가능하기 때문!!!

 

근데 scrollToItem은 viewWillAppear 이후의 시점에서만 동작(?)하는게 문제였다.

화면이 나타난 후, scrollToItem이 동작하니까 사용자 입장에서 인덱스상 첫 번째 카드가 보였다가 선택한 카드로 뿅 이동하는 요상한(?) 형태가 되어버렸다.

 

그래서 이걸 인디케이터를 띄워야 되나.. 런치스크린처럼 다른 화면을 잠시 보였다가 scrollToItem이 완료되면 dismiss하는 형태로 가야 되나 고민을 하던 중에 멘토님께서 해결방법을 알려주셔서 적용했고, 잘 해결되었다.

 

viewDidLoad시점에서 scrollToItem의 코드를 메인 스레드로 보내면 코드가 잘 동작했다.

이유는 좀 더 찾아봐야 할 것 같다.. 

 

 

 

그리고 프로젝트 초반에는 카드를 조회하는 화면에 라이브러리를 쓰지 않고, CollectionView + isPaging으로 구현했는데,

이게 문제가 페이징 상태에서 scrollToItem이 동작하질 않았다..

그래서 scrollToItem을 사용하기 전후로 isPaging 속성을 true, false 토글 해주는 방식을 사용했는데

문제는 CollectionView FlowLayout을 사용했을 땐 페이징 속성을 토글 할 수 있어도

Compositional Layout을 사용하면 페이징 속성을 Layout코드에서 적용하기 때문에 추후에 토글이 불가능했다.

(그리고 isPaging속성을 토글 하면서 scrollToItem을 사용하면 이동 후 레이아웃이 종종 깨지는 문제도 발생했음)

 

문제는 여행카드를 조회하는 화면은 레이아웃을 디바이스 width를 기준으로 비율로 잡았기 때문에 Flow Layout을 사용할 수 있었지만

날짜별 카드를 조회하는 화면은 문장을 보여줘야 해서 높이를 디바이스 크기에 맞게 최대한 늘려줄 필요가 있었고, Compositional Layout의 사용이 필요했다.

여기서 고민을 정말 많이 했다.. 카드의 동적 높이를 포기할지, 아니면 코드가 좀 지저분해져도 FlowLayout을 사용하여 디바이스 크기에 맞는 동적 높이를 제공할지..!!

 

뭐라도 다른 방법이 있지 않을까 라는 심정으로 구글링만 엄청 하다가 원인을 발견했는데...

어이없게도 페이징 상태에서 scrollToItem이 동작하지 않는 이슈는 iOS 14에서만 발생되는 이슈였다.. ㅋㅋ 13에서도 잘 돌아감!!!ㅠ

https://developer.apple.com/forums/thread/663156

 

iOS 14를 최소 타깃으로 설정했어서 시뮬을 14로만 돌렸던 게.. n시간의 삽질을 .. 하ㅠㅠ 

결론적으로 Compositional Layout을 사용하면서 페이징을 온오프하는 방법을 찾긴 했다. (.invalidateLayout() 메서드를 사용)

근데 스크롤이 넘어가는 애니메이션이 보이지도 않고.. 좀 엉성하게 구현되어서 결국 최소 버전을 iOS 15로 올리게 되었다..

 

 

 

 

FSPagerView 라이브러리 적용

프로젝트를 좀 진행하던 중, 카드를 조회하는 화면이 좀 밍밍해서 이쁘게 만드려고 시도했었다.

아래 사진처럼 좌우로 옆 카드가 살짝 나오고, 이쁘게 애니메이션 효과도 넣어주려고 했다.

 

처음에는 라이브러리 없이 만들어보고 싶어서 열심히 구글링을 하다가 collectionview carousel effect (회전목마처럼 이펙트를 준다) 키워드로 원하는 정보를 찾긴 했는데, 코드가 너무 어려웠다..

그래서 일단 라이브러리로 적용하고 나중에 덜어내기로 했다..!!

 

FSPagerView라는 라이브러리를 사용했는데, 이건 CollectionView를 뷰 객체로 사용하는 게 아니라 PagerView라는 뷰 객체를 따로 사용했기 때문에 이미 만들어진 구조를 뜯어내는 게 조금 귀찮긴 했다.. (그래도 결과물이 이뻐서 만족..!!)

 

 

 

 

백업 / 복구

Realm파일 자체를 백업하고 복구하면 앱을 재실행해야 적용되는 문제(?)가 있어서 JSON으로 백업 / 복구를 구현했다.

첨에 "realm to json in swift" 이런 키워드로 구글링을 좀 해봐도 정보도 별로 없고.. 아주 오래된 라이브러리만 나와서 멘토님께 도움을 요청했더니 Codable 키워드를 알려주셨다. (왜 이걸 생각 못했을까...)

Codable 키워드로 구글링을 해보니 그래도 정보가 좀 나와서 다소 짧은? 삽질을 한 후 JSON을 사용한 백업/복구를 구현할 수 있었다.

 

첨에는 어차피 API를 통해 데이터를 가져오는 것도 아니라서 CodingKeys, encode(to encoder:) 메서드를 따로 작성할 필요가 없을 줄 알았다. 그래서 Codable 프로토콜만 채택하고 바로 인코딩을 시도했는데 인코딩이 안되더라..ㅠ

근데 신기하게 작성 뷰컨에서 생성되는 객체를 바로 인코딩하면 그건 또 성공했다.

 

그래서 방금 막 생성된 객체와 Realm에 저장되었다가 꺼낸 객체가 무슨 차이가 있을까 싶어서 프린트로 찍어보니 구조가 좀 달라져있었다.

왼쪽이 이제 막 생성된 객체고, 오른쪽이 인코딩을 위해 Realm에서 꺼낸 객체

여튼저튼 이렇게 Realm에 들어갔다 나오면 내부적으로 구조가 좀 바뀌어서 CodingKeys, encode(to encoder:)를 작성해야 했던 것 같다. 

https://github.com/realm/realm-swift/issues/7616

위 게시글에서 힌트를 얻었고 잘 구현이 되었다.

 

 

위 내용들 말고도 이슈나 .. 고민했던 부분은 분명 더 있지만

유독 기억나는 내용들만 일단 작성했다.!

 

 

 

추후 업데이트할 내용

  1. 정렬 기준 변경 기능 추가 (UI 어디에 넣어야 좋을지 고민 중)
  2. 날짜별 이미지를 작성화면에서 한번에 넣는 기능 추가 (직접 넣어보니 하나씩 넣으면 귀찮..)
  3. 런치스크린 만들기
  4. 지역 입력하는 방식 개선 (map + 검색)
  5. 테마 색상을 사용자가 커스텀하게 만들 수 있도록 기능 구현 (ColorPickerViewController)
  6. MVVM 패턴 적용 (지금은 작성 화면만 MVVM 적용 중)
  7. 비효율적인 로직 있는지 확인하고 개선
  8. 저장된 여행지를 바탕으로 다음 여행지를 추천해주는 화면(?)

 

 

후기

처음으로 직접 앱을 출시해보니 신경 쓸 부분이 정말 엄-청 많다는 것을 알게 되었다.

특히 기획과 디자인의 중요성을.. 뼈저리게 느꼈다..

 

기획을 처음에 꼼꼼하게 작성하지 않았더니 계속 수정이 필요했는데.. 일단 열심히 하면 괜찮겠지 라는 잘못된 판단으로 중간에 수정을 하지 않고 진행했다.

그랬더니 매일 개발할 범위가 명확하게 보이지 않아서 이도저도 아닌 개발을 진행했던게 정말 큰 실수였던 것 같다.

기능 개발하다가 디자인 고치다가... 뭐 하나 만들어지면 그 다음에 뭘 만들지 고민하고.. 이렇게 낭비된 시간이 너무 많았다.

프로젝트 막바지에 잘못되었음을 깨닫고, 계획을 간단하게라도 짜면서 마무리 지었지만..

처음부터 기획을 잘 했더라면.. 수정이 필요했을 때, 시간을 조금만 투자해서 수정하고 개발을 했더라면.. 이라는 후회가 많이 들었다.

 

그리고 디자인은 내가 보기에 구린데 어딜 어떻게 고쳐야 할지 막막하니까 고통스러웠다..

코드는 기능 구현하는 로직이 떠오르지 않으면 계속 고민하면 해결 방법이 떠올랐는데

디자인은 아무리 고민해도 답이 안 나왔다... 고민한 시간은 많은데 성과는 없었다..ㅎㅠ

 

순수 개발만 하는 게 정말 행복한 거구나(?)도 느낄 수 있던 아주 좋은 경험이었다..

 

 

 

 

 

 

반응형