Conversation
Update issue templates
Create PULL_REQUEST_TEMPLATE.md
Rename .github/.github/PULL_REQUEST_TEMPLATE.md to .github/PULL_REQUE…
탭뷰 영역에 속하지 않기때문에 따로 빼기
…nfoPlist Client ID 값 추가
| import UIKit | ||
| import Login | ||
|
|
||
| class AppDelegate: NSObject, UIApplicationDelegate { |
There was a problem hiding this comment.
onOpenURL 을 사용하지 않고 Appdelegate를 따로 선언하신 이유가 궁금합니다!
There was a problem hiding this comment.
onOpenURL을 활용해 구현했다면 보다 간결하게
struct LoginView: View {
var body: some View {
VStack {
GoogleSignInButton()
}
.onOpenURL { url in
LoginConfiguration.handleOpenURL(url)
}
}
}과 같이 구현이 가능했을 것 같습니다. 하지만 View 메모리가 활성화 된 이후 시점에서 사용 가능하며, 가능성이 적을 순 있지만 AppDelegate로 구현하는 방식보다 안정성이 다소 떨어진다고 생각했습니다~
또한, 추후 FCM을 활용할 예정이기 때문에 확장성 측면에서 현재 시점부터 AppDelegate로 구현하는 방식으로 선택했습니다.
| public init() {} | ||
|
|
||
| public func signIn() async throws -> (accessToken: String, name: String?, email: String?) { | ||
| guard let scene = await UIApplication.shared.connectedScenes.first as? UIWindowScene, |
There was a problem hiding this comment.
UIApplication 는 메인스레드에서만 접근 가능한데 async 컨텍스트에서 직접 사용이 되고있어요
| guard let scene = await UIApplication.shared.connectedScenes.first as? UIWindowScene, | |
| guard let scene = await MainActor.run { UIApplication.shared.connectedScenes.first } as? UIWindowScene, | |
| let root = await MainActor.run { scene.windows.first?.rootViewController } else { |
이런식으로 MainActor로 돌리는것이 좋을것같아요
There was a problem hiding this comment.
감사합니다 :)
전달 주신 수정사항에 대해서 살짝 수정해 이런 방식으로 수정하는 건 어떻게 생각하시나요?
let rootViewController: UIViewController? = await MainActor.run {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else {
return nil
}
return root
}
guard let root = rootViewController else {
throw NSError(
domain: "GoogleAuthService",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "RootViewController를 찾을 수 없습니다."]
)
}There was a problem hiding this comment.
앗 이방법이 더 나은방식인것같네요 제가말한방식은 scene 변수가 첫 번째 MainActor.run 블록 밖에서 정의되지 않아서 두 번째 블록에서 사용할수가 없을거라 이방식이좀더 맞는방법인것같네요!
There was a problem hiding this comment.
더 근본적인 질문이 있어요..!
제가 코드를 다 본 것은 아니지만 GoogleAuthService에서 rootViewController를 찾는 이유가 뭘까요~?
UI 관련 객체가 아닌 것 같은데 UI에 접근하고 있어서 import UIKit 의존성도 생기고 mainActor 이슈도 생기는 것 같아서 코멘트 남깁니당
There was a problem hiding this comment.
말씀 해주신 내용을 바탕으로 다시 고민해보니 Service에서 UI에 접근하기 때문에 추가 의존성과 MainActor 이슈가 발생하는 것 같습니다. Service에서는 비즈니스 로직/API통신만 담당 하도록 그 책임을 명확하게 할 필요가 있을 것 같아요.
LoginWithGoogleUseCaseInterface를 구성해서 UI에 접근하는 로직을 별도 구성하고 현재 Store에서 추가적으로 담당하고 있는 handleGoogleSignIn로 해당 UseCase에서 구현하여 Store가 불필요한 로직을 가지고 있지 않도록 하는 방식으로 시도해보겠습니다.
| let root = await scene.windows.first?.rootViewController else { | ||
| throw NSError( | ||
| domain: "GoogleAuthService", | ||
| code: -1, |
There was a problem hiding this comment.
아직 에러처리가 미정이라 하드코딩으로 넣은것으로 생각하겟습니다!
There was a problem hiding this comment.
넵넵!! 맞습니다 그렇게 생각해주십셔!!
| import ServiceInterface | ||
|
|
||
|
|
||
| public final class GoogleAuthService: GoogleAuthServiceInterface { |
There was a problem hiding this comment.
(개인의견)
SOLID 원칙에 의거하여 코드를 구현하는거에 대해서 논의를 하지 않았지만 해당 클래스가 인증 + 네트워크 + 로깅 이 한번에 있어서 SRP를 준수하지 못하고 있는것 같습니다.
해당 클래스는 인증만을 가지고 토큰 관련 부분을 다른 클래스로 빼는것도 방법이 될수 있을것같아요!!
There was a problem hiding this comment.
좋은 말씀이신 것 같습니다. 제가 생각했던건 톡방에서 여쭤봤던 것 처럼 네트워크 관련 로직들 ( + 로직 세분화 ) 별개로 처리할지 통합해 처리할지 관련해서 나왔던 말이 따로 하지 않고 플랫폼 Service에 한번에 구현하는 방식으로 하자고 이해했었는데 한번 더 논의해보는 것으로 하면 좋을 것 같습니다.
개인적으로는 의견 주신 내용처럼 단일 책임을 가진 형태로 구현하는 방식이 좋을 것 같다고 생각합니다 :)
| let parameters: [String: String] = ["access_token": accessToken] | ||
|
|
||
| do { | ||
| let response = try await AF.request( |
There was a problem hiding this comment.
alarmofire에 직접 의존을 하면 추후 테스트 코드 넣을때 테스트가 어려울것이라 생각합니다.
DI로 추상화 해서
protocol NetworkService {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}이런식으로 의존성을 주입하는 형식으로 대체 가능해보일듯 합니다
There was a problem hiding this comment.
말씀하신 것 처럼 추후 애플 로그인도 같은 요청을 서버에 줄 수 있을 것 같아서 각각 직접 Alamofire에 의존해서 구현하게 되는 문제도 발생할 수 있겠네요!
의견 주신 것처럼 LoginNetworkServiceProtocol 을 만들어 요청 로직을 추상화하고 각각 적용하는 방식으로 수정하도록 하겠습니다.
| .project(target: "UseCase", path: "../Core"), | ||
| .project(target: "Repository", path: "../Core"), | ||
| .project(target: "Network", path: "../Core") |
There was a problem hiding this comment.
Core에서 피쳐를 의존하지 않으면 순환 참조가 생기진 않아요~!
코어는 피쳐를 당연히 몰라야 하지만 피쳐도 코어를 몰라야 하는가? 하면 그건 고민해보시면 좋겠네요 ㅎㅎ
There was a problem hiding this comment.
이 부분도 저희가 회의했던 것 처럼 모듈 구조 픽스 되면 참고해서 수정 진행하도록 하겠습니다 :)
| let accessToken = user.accessToken.tokenString | ||
|
|
||
| // 토큰 검증 (원래 코드와 동일) | ||
| await verifyGoogleToken(accessToken) |
There was a problem hiding this comment.
이부분도 위에서 말햇듯 Google SDK로 받은 토큰을 Google API로 다시 검증하고 있어서 의미없는 코드라고 생각됩니다
There was a problem hiding this comment.
위에 답변 드린 내용과 같이 동일하게 수정하도록 하겠습니다 :0
| import Common | ||
| import UIKit | ||
|
|
||
| public final class DefaultGoogleAuthService: GoogleAuthServiceInterface { |
There was a problem hiding this comment.
Projects/Core/Network/Sources/GoogleAuthService 의 코드랑 같은 코드인거같은데 어떤 차이점이 있나요?
There was a problem hiding this comment.
기존에 ThirdParty 로 구현했던 GoogleAuthService 내용을 삭제하고 Network 모듈로 변경했는데 generate시 서드파티가 확인되지 않아 삭제가 누락 됐던 것 같습니다 :)
해당 내용 삭제 하였습니다!
|
|
||
| public struct LoginUser { | ||
| public let email: String | ||
| public let accessToken: String? |
There was a problem hiding this comment.
로그인을 성공했는데 엑세스토큰이 nil로 올수 있나요?
로그인이 고장난상태에서 개발중 UI작업을 할때를 상정한 코드인가요?
There was a problem hiding this comment.
이 부분도 서버 API 명세서가 나온다면 수정될 수 있는 부분이라고 생각됩니다. 해당 내용 구현 시 생각했던 내용은 accessToken이 nil인 상황에 대해 대비하기 위해 옵셔널로 구성하였습니다.
| @StateObject private var store: LoginStore | ||
|
|
||
| public init(store: LoginStore) { | ||
| self._store = StateObject(wrappedValue: store) | ||
| } |
There was a problem hiding this comment.
해당부분을 StateObject로 작업하신 이유가 궁금합니다
There was a problem hiding this comment.
@StateObject가 "이 View가 객체의 소유자"라는 의미인데, 외부에서 이미 생성된 store를 받아서 다시 StateObject로 감싸고있는것같아요
// 지금 일어나는 일
struct ParentView: View {
@StateObject var loginStore = LoginStore() // 첫 번째 참조
var body: some View {
LoginView(store: loginStore) // 전달
}
}
struct LoginView: View {
@StateObject private var store: LoginStore
init(store: LoginStore) {
// 같은 객체에 대해 두 번째 참조 발생
self._store = StateObject(wrappedValue: store)
}
}이렇게 되면 같은 LoginStore 객체에 대해 두 개의 서로 다른 참조가 생기고 있어요
SwiftUI가 그래서 객체 담당을 누가 해야되는데?!?!?!?!?!?!?!?!? 하면서 혼란스러워지면서 메모리 누수가 발생되지 않을까 싶네요
public struct LoginView: View {
@ObservedObject private var store: LoginStore // StateObject → ObservedObject
public init(store: LoginStore) {
self.store = store // 단순 할당, 추가 래핑 없음
}
}@ObservedObject는 "나는 이 객체를 관찰만 할 뿐, 소유하지는 않는다"는 라고 가져가기에 소유권은 외부에서 관리하고, View는 단순히 관찰만 하므로 생명주기가 좀더 이뻐지고 추후 저희의 MVI에 좀더 맞는 방식이라 생각이 되어요!
There was a problem hiding this comment.
구글 로그인 로직을 테스트하면서 단일 프로젝트에서 크게 신경쓰지 않고 StateObject를 사용해 구현했던 것 같습니다.
말씀해주신 내용과 같이 현재 StateObject를 사용하면서
@StateObject private var store: LoginStore
public init(store: LoginStore) {
self._store = StateObject(wrappedValue: store) // ❌
}객체의 소유자인 상태에서 불필요한 생성자 활용으로 인해 잘못 구성되어있습니다.
현재와 같은 상태에서 외부에서 생성된 store 객체를 전달 받는 상태에서는 ObservedObject를 활용해
@ObservedObject private var store: LoginStore
public init(store: LoginStore) {
self.store = store // ✅ 전달받은 인스턴스 사용
}이와 같은 방법으로 수정하도록 하겠습니다 :)
|
PR 중간의 스웨거 링크는 지우는게 좋을것같아요! |
lsj8706
left a comment
There was a problem hiding this comment.
다들 열심히 하는 모습 멋지네요!!
슬쩍 보고 눈에 보이는 것들 위주로 코멘트 남겼어요. 제가 드린 의견/질문은 다 반영하려고 하지 마시고 팀원분들이랑 고민해보면 좋을 것 같아요.
그리고 커밋 내역을 보면 아마 작업하면서 깃이 좀 꼬이신 것 같아요.
완전 동일한 커밋들이 여러개 있고 컨플릭트 상태였던 커밋도 있어 보여요.
큰 작업일수록 커밋 단위로 리뷰하는 것이 리뷰어에게 편하기 때문에 이 부분도 신경써서 맥락단위로 나누는 연습 해보시면 정말 도움 많이 될 것 같아요!! 👍
Projects/Feature/Project.swift
Outdated
| .project(target: "Domain", path: "../Interface"), | ||
| .project(target: "DesignSystem", path: "../Shared"), | ||
| .project(target: "LoginInterface", path: "../Interface"), | ||
| .project(target: "Common", path: "../Shared") |
There was a problem hiding this comment.
이렇게 문자열을 직접 기입해서 의존성을 관리하면 오타 등의 실수가 발생하기 쉬울 것 같아요.
모듈을 모델링해서 다음과 같이 사용하면 더 좋을 것 같네요.
짧게 생각해본 예시 코드
.feature(interface: .logIn)
.shared(module: .designSystem)There was a problem hiding this comment.
최대한 휴먼 에러를 줄일 수 있도록 ProjectDescriptionHelper를 도입하는 방식도 고려해 보도록 하겠습니다 :)
| public struct UserEntity { | ||
| public let email: String | ||
| public let isExistingUser: Bool | ||
| public let accessToken: String |
There was a problem hiding this comment.
UserEntity 도메인 모델에 대한 설계를 더 엄격하게 하면 좋을 것 같아요..!
- email -> Ok
- isExistingUser -> 이 값이 왜 필요한지 잘 모르겠어요.
- accessToken -> 인증 정보에 가깝고 유저 도메인 모듈에 있으면 안 될 것 같음
정말 유저 정보에 해당하는 필드들만 들어가는게 더 좋은 모델 설계일 것 같아요~!
There was a problem hiding this comment.
여기 말고도 전반적으로 모델링/인터페이스를 디자인할 때 다음과 같이 기준을 세우고 시작하면 좋을 것 같아요!
- 네이밍이 의도를 잘 드러내는가
- 이 속성/메서드가 의도에 부합한가 (객체의 역할에 속하는가)
There was a problem hiding this comment.
기존에는 도메인 모델/인터페이스 작성 시 의도를 깊게 생각하지 않았던 것 같아요.
의견 주신 내용과 같이 도메인 역할에 맞는 필드들만 들어갈 수 있도록 수정하도록 하겠습니다. 그리고 추후에도 네이밍 의도가 잘 드러나는지, 역할에 맞는지 등 그 의도에 대해서 다시 생각해보고 설계할 수 있도록 노력하겠습니다!
| public protocol LoginInterfaceProtocol: ObservableObject { | ||
| var events: [Event] { get } | ||
| var selectedDate: Date { get set } | ||
| var isLoading: Bool { get } | ||
|
|
||
| func loadEvents(for date: Date) async | ||
| func selectDate(_ date: Date) async | ||
| } |
There was a problem hiding this comment.
인터페이스를 만들 때는 그 의도가 명확하게 들어나는 것이 좋아요!
LoginInterfaceProtocol-> 이걸 봤을 때는 로그인과 관련된 일을 수행하는 친구일 것이라고 생각했어요.
그런데 이 인터페이스가 명시하는 속성, 메서드는 로그인과 무관해 보이는 이름들이에요. (실제로 내부 구현은 로그인과 관련이 있긴 하겠지만)
직관적으로 느낀 바를 적어보면
event는 어떤 이벤트인지 살짝 감이 오지 않아요.selectedDate-> 날짜와 로그인이 어떤 관계가 있는지 잘 모르겠어요.- 다른 것들도 비슷한 느낌이에요.
제가 배경 맥락을 모르고 없이 그냥 적어보자면 LoginInterfaceProtocol은 다음과 같은 구성일 것이라고 예상해요.
public protocol LoginInterfaceProtocol {
func logIn(id: String, token: String) async throws
}|
|
||
| public struct LoginRequestDTO: Encodable { | ||
| public let accessToken: String | ||
| public let name: String? |
There was a problem hiding this comment.
현재 단계에서 서버와 테스트를 위해 신규회원 가입과 그냥 로그인을 나눠 테스트 하느라 그 차이를 name으로 나눠 진행하고 있어요 :)
추후 완성된 API 명세를 받으면 수정 될 예정입니다~~!
신경써서 리뷰 달아줘서 고마워용~~
| import Common | ||
|
|
||
| @MainActor | ||
| public final class LoginStore: ObservableObject { |
There was a problem hiding this comment.
질문
타 모듈에서 사용되는 타입일까요? 아니라면 internal로 지정해도 괜찮을 것 같아요.
다른 타입들도 마찬가지에요!
| state.errorMessage = "" | ||
| state.isLoading = true |
There was a problem hiding this comment.
참고차 코멘트 남겨보자면, 현재 아키텍처 상으로는 state를 변경하는 곳이 store 내부라면 어디든 가능하기 때문에 안정적인 단방향 플로우는 아닌 것 같아요!
-> 데이터 레이스 발생 가능 + 상태 업데이트 순서 보장이 어려움
나중에 고도화 하게 되면 팀원분들이랑 개선 포인트 찾아보면 좋을 것 같아요~!
| public init() {} | ||
|
|
||
| public func signIn() async throws -> (accessToken: String, name: String?, email: String?) { | ||
| guard let scene = await UIApplication.shared.connectedScenes.first as? UIWindowScene, |
There was a problem hiding this comment.
더 근본적인 질문이 있어요..!
제가 코드를 다 본 것은 아니지만 GoogleAuthService에서 rootViewController를 찾는 이유가 뭘까요~?
UI 관련 객체가 아닌 것 같은데 UI에 접근하고 있어서 import UIKit 의존성도 생기고 mainActor 이슈도 생기는 것 같아서 코멘트 남깁니당
| .project(target: "UseCase", path: "../Core"), | ||
| .project(target: "Repository", path: "../Core"), | ||
| .project(target: "Network", path: "../Core") |
There was a problem hiding this comment.
Core에서 피쳐를 의존하지 않으면 순환 참조가 생기진 않아요~!
코어는 피쳐를 당연히 몰라야 하지만 피쳐도 코어를 몰라야 하는가? 하면 그건 고민해보시면 좋겠네요 ㅎㅎ
- Service 계층에서 UI 로직 제거 및 책임 분리 - UseCase 계층 UI 의존성 제거 및 ViewControllerProvider 도입 - Store 계층 단순화 및 AuthUseCase 분리 - Repository 구현체 및 DTO 매핑 로직 추가 - Apple 로그인 기능 추가 (Service, UseCase, DTO) - 전체 계층 인터페이스 추상화로 테스트 가능성 개선
| public init() {} | ||
|
|
||
| public func signIn() async throws -> (accessToken: String, name: String?, email: String?) { | ||
| guard let scene = await UIApplication.shared.connectedScenes.first as? UIWindowScene, |
There was a problem hiding this comment.
앗 이방법이 더 나은방식인것같네요 제가말한방식은 scene 변수가 첫 번째 MainActor.run 블록 밖에서 정의되지 않아서 두 번째 블록에서 사용할수가 없을거라 이방식이좀더 맞는방법인것같네요!

🔗 연결된 이슈
💻 주요 코드 설명
1. Entity (Domain Layer)
2. Interface Layer
Repository와 외부 서비스들을 인터페이스로 추상화 구현했습니다
3. UseCase
로그인 관련 비즈니스 로직을 담당합니다.
4. Repository
네트워크 통신을 통한 서버 API 호출과 DTO → Domain Entity 변환을 담당합니다.
5. DTO (Data Transfer Object) - 서버 통신 데이터 구조
전달 받은 Swagger를 바탕으로 서버 통신 데이터 구조를 정의하고, 조건부 인코딩으로 기존/신규 사용자를 구분 처리하도록 구현하였습니다.
6. GoogleAuthService
7. NetworkService (Infrastructure Layer) - HTTP 통신
Alamofire를 활용한 HTTP 통신을 추상화하여 구현하였습니다.
8. View Layer (Presentation) - 사용자 인터랙션
사용자 터치 이벤트를 Intent로 변환하여 Store에 전달하며, 로딩 상태에 따른 UI 업데이트를 처리합니다.
9. Store Layer (Presentation) - 상태 관리 및 플로우 제어
TCA 패턴의 Intent-State-Effect 구조로 단방향 데이터 플로우를 구현하고, 사이드 이펙트를 분리하여 관리합니다.
10. 복합 로그인 플로우 - 기존/신규 사용자 자동 구분
2단계 로그인 시도를 통해 기존 사용자는 즉시 로그인하고, 신규 사용자는 자동으로 회원가입 처리하여 방식으로 전달받아 임시적으로 이런 플로우로 개발!
11. 앱 설정 및 초기화
Google SDK 초기화와 URL 스킴 처리를 Login 모듈 내부로 캡슐화하여 관심사를 분리했습니다.
로그인 로직 데이터 플로우
이전에 구현해왔던 내용을 현재 구조에 맞게 재구현하여 넣다보니 양이 너무 많은 것 같아요 ㅠㅠ 죄송합니다.
이후로는 좀 쪼개서 진행하도록 하겠습니다.
해당 구글 로그인 로직은 서버와 연동 테스트를 마무리한 코드입니다. 13일 변경된 서버 API 명세서를 확인 한 뒤 로직 변경있을 예정입니다.
✔️ CI Completed
✔️ CI Completed
✔️ CI Completed