Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI 챗봇 앱 [STEP 3] Jin, Harry #48

Open
wants to merge 9 commits into
base: d_Jin
Choose a base branch
from

Conversation

hemil0102
Copy link

@hemil0102 hemil0102 commented Apr 12, 2024

안녕하세요 July(@July911), 진 해리입니다.
STEP3를 하는 동안 벚꽃이 다 지고 날이 점점 따듯해 지고 있는 것 같습니다.
우리의 앞길도 따듯해졌음 하는 작은 바람이 생깁니다.

처음으로 MVVM을 적용하기에 리팩토링이나, 역할 책임 분산이 다소 미흡합니다.
그렇지만 기능 구현은 되었다 생각하여 우여곡절 끝에 작업한 STEP3 PR 보내드립니다!

잘 부탁드리겠습니다. 😉

🔍 What is this PR?

  • ✅ Step 2에서 구현한 네트워킹 기능을 통해 실제로 대화 화면을 구현합니다.
  • ✅ 하나의 대화 흐름이 하나의 채팅방에 기록될 수 있도록 구현합니다
    ex) 처음에 API에게 이름을 말해주면, 대화 내내 API가 그 이름을 기억하고 말할 수 있다.
  • ✅ 대화 화면은 Collection View를 활용해서 구현해보도록 합니다.
  • ⚠️ 응답 메세지가 수신중일 경우, 메세지가 로딩중인 것을 말풍선 안에 보여줄 수 있도록 합니다.
    Core Graphics, Core Animation, UIView Animation 등을 사용해서 자유롭게 만들어주세요
  • ✅ 대화를 스크롤 할 때, 키보드가 내려갈 수 있도록 합니다.
  • ✅ 키보드가 올라가고 내려갈 때, 입력칸이 키보드를 따라다닐 수 있도록 구현합니다.
    KeyboardLayoutGuide를 활용해보세요.
  • ✅ 네트워크 문제 등 API Response 수신에 문제가 발생할 경우 사용자가 알 수 있도록 합니다.
    문제가 발생할 경우, 사용자에게 알려준 후 다시 요청 할 수 있도록 구현해보세요.

🗂️ 아키텍처 및 기술스택

MVVM + Clean Architecture + Combine + UIKit

📝 PR 고민 Point

1. MVVM
Combine @published를 통해서 옵저빙이 가능하도록 뷰모델을 만들었습니다.
이후 ViewController를 뷰로 활용하여 바인딩을 통해 데이터의 상태가 변화하면 뷰가 업데이트 되도록 역할을 나누었습니다.

2. GPT 이전 대화 기록
history += "UserQuestion(count): (query)" + " "
history += "GPTAnswer(count): (answer[0].content)" + " "

위와 같이 history에 대화들을 계속 누적하게 만들어 대화형식으로 표현을 하고
질문을 할 때 history + question 형식으로 질문을 할 때 이전 대화 기록을 같이 보내 주는 방식으로 만들었습니다.

3. CollectionViewListCell Reusable
prepareForReuse를 활용하여 셀의 레이아웃 제약을 다시 그려줄 수 있도록 했습니다.

4. 말풍선
User질문과 GPT답변을 구분하여 레이아웃을 그려주는 방법은 아래와 같습니다.

var viewModel: ChatRoomCellViewModel! {
    didSet { setupViewModel() }
}

private func setupViewModel() {

    bubbleView.chatLabel.text = viewModel.contentMessage
    bubbleView.outcoming = viewModel.contentRole

    leadingOrTrailingConstraint.isActive = false
    if viewModel.contentRole == .user {
        leadingOrTrailingConstraint = bubbleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12.0)
    } else {
        leadingOrTrailingConstraint = bubbleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12.0)
    }
    leadingOrTrailingConstraint.isActive = true
}

viewModel.contentRole == .user 유저 메시지인지 아닌지에 따라 bubleView를 오른쪽 왼쪽으로 띄워줍니다.

5. 로딩창
시간 부족으로 애니메이션 관련된 부분은 우선 순위를 낮춰서 하여 말풍선이 로딩하는 것은 구현하지 못했지만 답변이 오는 동안 로딩 표시는 됩니다.

❓ 질문 Point

1. MVVM
인터넷에 예제를 참고하여서 처음으로 MVVM을 구현해봤습니다. 미흡한 부분이 많이 있을텐데 최대한 리뷰를 부탁드리며, ChatRoomCellViewMode에서 @published로 선언된 부분은 바인딩으로 사용되는 부분이 없는 것 같아 @published을 달아줄 필요가 없을 것 같은데... 예제에는 달려 있어서 남겨주었지만, 저희가 생각하고 있는 것이 맞는지 궁금합니다.

2. GPT 이전 대화 기록
현재의 방식은 질문에 이력을 추가해서 보내는 양식인데, 이런 이전 대화 기록 부분을 .system에 업데이트해서 관리하는 것이 더 나을지? 이전 대화 기록을 조금 더 잘 관리할 수 있는 부분이 궁금합니다.

3. 마지막에 발생한 GPT 이전 대화 기록 관련 버그
아래 history += "GPTAnswer(count): (answer[0].content)" + " "
가 한번만 호출되게 하고 싶은데 viewModel.$comments 이후로 안에 동작이 2번 이뤄집니다. 왜 두번 호출되는지 잘 모르겠습니다. 어떻게 디버깅을 해볼 수 있을까요?

func bindViewModelToView() {
            viewModel.$comments
                .receive(on: RunLoop.main)
                .sink(receiveValue: { [weak self] answer in
                    guard let self = self else { return }
                    if !answer.isEmpty {
                        print("호출 두번되는 문제가 있음")
                        history += "GPTAnswer\(count): \(answer[0].content)" + " "
                        self.count += 1
                        self.updateSections()
                    }
                })
                .store(in: &bindings)
  1. 모델 구현을 조금 더 잘해볼 수 있는 방법?

📸 구동화면

Simulator Screen Recording - iPhone 15 Pro - 2024-04-12 at 16 41 23

case chat
}

struct ChatRoomModel: Hashable {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatRoomModel 은 좋지않은 이름입니다. 모든게 다 model아닌가요 ?

return label
}()

var outcoming = Role.user
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적절한 이름인가요 ?

commonInit()
}

private func commonInit() -> Void {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. layout을 잡는 코드같은데 왜 이름이 init인가요 ?
  2. -> Void의 용도는 무엇인가요 ?

])
}

override func layoutSubviews() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무엇을 해주는 메서드인가요 ?

import UIKit

final class ChatRoomCell: UICollectionViewListCell {
static let identifier = "ChatRoomCell"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른방법은 없을까요?


private lazy var contentView = ChatRoomView()
private let viewModel: ChatRoomViewModel
private var bindings = Set<AnyCancellable>()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 레퍼런스에서도 이런 이름으로 쓰였던가요 ?

var snapshot = Snapshot()
snapshot.appendSections([.chat])
snapshot.appendItems(viewModel.comments)
dataSource.apply(snapshot, animatingDifferences: true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentView.collectionView.scrollToItem(at: index, at: .bottom, animated: true) 는 컴플리션에 달아주는게 나아보이네요.

}

final class ChatRoomViewModel {
@Published private(set) var comments: [ChatRoomModel] = []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments: [ChatRoomModel] 어색한 것 같습니다.

case chattingFetch
}

enum ChatRoomModelState: Equatable {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 접근이네요.
ChatRoomVMState가 조금 더 나아보이네요.

}

private func setupBindings() {
func bindViewModelToView() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중첩하신 이유가 있을까요 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants