Feat/#3 네트워크 모듈화#5
Conversation
Walkthrough네트워크 모듈을 새로 추가하고, 인증(signup) API 호출용 DTO·서비스·타겟·에러 매핑·로거·비동기 Moya 확장과 도메인/데이터/DI 연결을 구현했습니다. 빈 플레이스홀더 파일들을 삭제하고 Info.plist에 Changes
Sequence Diagram(s)sequenceDiagram
participant App
participant Repository as AuthRepository
participant Service as AuthService
participant Provider as MoyaProvider
participant Network as NetworkLayer
participant API as Auth API
App->>Repository: signup(info: SignupInfo)
Repository->>Service: signup(request: SignupRequest)
Service->>Provider: request<SignupResponse, SignupError>(AuthAPI.signup)
Provider->>Network: 네트워크 요청 (async)
Network->>API: POST /api/v1/auth/users
alt 응답 코드 == "2000"
API-->>Network: BaseResponse<SignupResponse>
Network-->>Provider: 디코딩 성공
Provider-->>Service: NetworkResult.success(SignupResponse)
Service-->>Repository: .success(SignupResponse)
Repository-->>App: Result.success(SignupResult)
else 응답 코드 != "2000"
API-->>Network: BaseResponse + ErrorResponse
Network-->>Provider: ErrorResponse 디코딩
Provider-->>Service: NetworkResult.failure(SignupError) via errorMapper
Service-->>Repository: .failure(SignupError)
Repository-->>App: Result.failure(SignupError)
else 네트워크/디코딩 오류
Network-->>Provider: MoyaError / decoding error
Provider-->>Service: NetworkResult.networkFailure(NetworkError)
Service-->>Repository: .failure(.networkError(...))
Repository-->>App: Result.failure(SignupError.networkError)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 개요네트워크 모듈의 완전한 구현으로, 인증 가입 API 호출을 위한 기반 구조, 에러 처리, 데이터 전송 객체, 저장소 계층 및 의존성 주입 패턴을 추가합니다. 불필요한 빈 파일들을 제거합니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant App
participant AuthService
participant MoyaProvider
participant NetworkLayer as Moya Network
participant API as Auth API
App->>AuthService: signup(request: SignupRequest)
AuthService->>MoyaProvider: request<SignupResponse, SignupError>(...)
MoyaProvider->>NetworkLayer: 비동기 요청
NetworkLayer->>API: POST /api/v1/auth/users
alt API 성공 (코드: 2000)
API-->>NetworkLayer: BaseResponse<SignupResponse>
NetworkLayer-->>MoyaProvider: 데이터 디코딩
MoyaProvider-->>AuthService: NetworkResult.success(SignupResponse)
AuthService-->>App: Result<SignupResponse, SignupError>
else API 실패 (코드 != 2000)
API-->>NetworkLayer: BaseResponse + ErrorResponse
NetworkLayer-->>MoyaProvider: errorMapper 호출
MoyaProvider-->>AuthService: NetworkResult.failure(SignupError)
AuthService-->>App: Result with SignupError
else 네트워크 오류
API-->>NetworkLayer: MoyaError 발생
NetworkLayer-->>MoyaProvider: NetworkError.connectionFailed
MoyaProvider-->>AuthService: NetworkResult.networkFailure(NetworkError)
AuthService-->>App: Result with NetworkError
end
코드 검토 예상 소요 시간🎯 4 (복잡함) | ⏱️ ~60분 시
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift`:
- Around line 40-42: The code currently constructs SignupField(rawValue:
firstError.field ?? "") which conflates a missing field (nil) with an
empty/unknown raw value; change the logic to explicitly check firstError.field:
if firstError.field is nil then assign a distinct sentinel (e.g., set self.field
to nil if field is optional or to a dedicated enum case like .missingField),
otherwise attempt SignupField(rawValue: fieldRaw) and fall back to an explicit
unknown case for unparsable values; keep assigning self.fieldMessage =
firstError.message. Ensure you update code paths referencing field to handle the
new sentinel/optional case.
In `@Projects/Modules/Networks/Tests/AuthServiceTests.swift`:
- Around line 27-76: The test test_signup_실제API호출 in AuthServiceTests makes a
real network call via sut.signup which should be replaced with a unit-test using
a mocked MoyaProvider; update AuthServiceTests to inject a stubbed MoyaProvider
(e.g., MoyaProvider<AuthAPI> with stubClosure: .immediatelyStub) into the
AuthService instance (sut) and change test_signup_실제API호출 to assert
parsing/handling of the stubbed response instead of calling the real API, moving
the live-call test to a separate integration test suite.
🧹 Nitpick comments (10)
Projects/Modules/Networks/Sources/Base/NetworkError.swift (1)
11-26:LocalizedError프로토콜 채택 고려
message프로퍼티 대신LocalizedError프로토콜을 채택하면 Swift의 에러 처리 시스템과 더 자연스럽게 통합됩니다.♻️ 제안된 리팩토링
-public enum NetworkError: Error { +public enum NetworkError: LocalizedError { case connectionFailed case decodingFailed case unknown(Error) - public var message: String { + public var errorDescription: String? { switch self { case .connectionFailed: return "네트워크 연결을 확인해주세요" case .decodingFailed: return "데이터 처리 중 오류가 발생했습니다" case .unknown: return "알 수 없는 오류가 발생했습니다" } } }Projects/Modules/Networks/Tests/AuthServiceTests.swift (2)
9-10: Import 정렬 필요SwiftLint 경고에 따라 import 문을 알파벳 순으로 정렬하세요.
♻️ 제안된 수정
-import XCTest `@testable` import Networks +import XCTest
29-42: 디버깅용 print 문 제거 필요테스트 코드에 디버깅용
Also applies to: 47-51, 56-65, 69-71
Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift (1)
12-21:fatalError대신 빌드 타임 검증 또는 안전한 처리 고려
BASE_URL이 누락된 경우 앱이 즉시 크래시됩니다. 프로덕션에서는 다음 방안을 고려하세요:
- 빌드 스크립트에서 검증: xcconfig에 BASE_URL이 없으면 빌드 실패하도록 설정
- 캐싱:
baseURL이 호출될 때마다 번들을 탐색하는 대신 한 번만 계산♻️ 캐싱 적용 제안
public enum NetworkConfiguration { - public static var baseURL: URL { + public static let baseURL: URL = { // 테스트 환경에서는 Bundle.main이 Xcode 도구를 가리키므로 테스트 번들에서 찾음 let bundle = Bundle.allBundles.first { $0.bundlePath.hasSuffix(".xctest") } ?? Bundle.main guard let urlString = bundle.infoDictionary?["BASE_URL"] as? String, let url = URL(string: urlString) else { fatalError("BASE_URL not found in Info.plist") } return url - } + }() }Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift (1)
18-20: 암시적 반환으로 SwiftLint 경고를 정리해주세요.
스타일 경고 제거용으로만 추천합니다.♻️ 제안 수정
public var baseURL: URL { - return NetworkConfiguration.baseURL + NetworkConfiguration.baseURL } public var headers: [String: String]? { - return ["Content-Type": "application/json"] + ["Content-Type": "application/json"] }Also applies to: 43-45
Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift (1)
2-2: 파일 헤더명이 실제 파일명과 다릅니다.
문서 정합성을 위해 맞춰두는 편이 좋습니다.📝 제안 수정
-// Signin.swift +// SignupDTO.swiftProjects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (3)
15-57:request와requestPlain메서드 간 코드 중복 고려두 메서드의 응답 처리 로직(성공 코드 확인, 에러 응답 디코딩, continuation resume)이 거의 동일합니다. 공통 로직을 private 헬퍼로 추출하면 유지보수성이 향상될 수 있습니다.
♻️ 중복 제거를 위한 리팩토링 제안
private func handleResponse<T: Decodable, E: APIErrorProtocol>( _ result: Result<Response, MoyaError>, transform: (BaseResponse<T>) -> NetworkResult<T, E>? ) -> NetworkResult<T, E> { switch result { case .success(let response): do { let baseResponse = try JSONDecoder().decode( BaseResponse<T>.self, from: response.data ) if baseResponse.code == Self.successCode { if let transformed = transform(baseResponse) { return transformed } return .networkFailure(.decodingFailed) } else { let errorResponse = try JSONDecoder().decode( ErrorResponse.self, from: response.data ) let apiError = E( code: errorResponse.code ?? "", message: errorResponse.message ?? "", errors: errorResponse.errors ?? [] ) return .failure(apiError) } } catch { return .networkFailure(.decodingFailed) } case .failure(let moyaError): return .networkFailure(Self.mapMoyaError(moyaError)) } }Also applies to: 59-97
13-13: 성공 코드에 대한 문서화 고려
"2000"성공 코드가 하드코딩되어 있습니다. 이 값의 의미와 출처(API 스펙)에 대한 주석을 추가하면 향후 유지보수에 도움이 됩니다.
99-107: 추가 네트워크 에러 케이스 매핑 고려현재
NSURLErrorDomain만.connectionFailed로 매핑됩니다. 타임아웃(NSURLErrorTimedOut), 취소(NSURLErrorCancelled) 등 세분화된 에러 처리가 필요하다면 추가 매핑을 고려해 보세요.♻️ 세분화된 에러 매핑 예시
private static func mapMoyaError(_ error: MoyaError) -> NetworkError { switch error { case .underlying(let nsError as NSError, _) where nsError.domain == NSURLErrorDomain: - return .connectionFailed + switch nsError.code { + case NSURLErrorTimedOut: + return .timeout + case NSURLErrorCancelled: + return .cancelled + default: + return .connectionFailed + } default: return .unknown(error) } }Note:
NetworkError에 해당 케이스 추가가 필요합니다.Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift (1)
48-52: 에러 코드에 대한 문서화 권장
"COMM-01-005","COMM-08-001"등 에러 코드의 의미와 출처를 주석으로 남기면 팀원들이 코드를 이해하는 데 도움이 됩니다.// API 에러 코드 매핑 (참고: API 스펙 문서 링크) // COMM-01-005: 요청 검증 실패 // COMM-08-001: 내부 서버 오류 switch code { case "COMM-01-005": self.type = .validationError case "COMM-08-001": self.type = .internalServerError default: self.type = .undefined(code: code) }
| if let firstError = errors.first { | ||
| self.field = SignupField(rawValue: firstError.field ?? "") | ||
| self.fieldMessage = firstError.message |
There was a problem hiding this comment.
field가 nil일 때의 처리 확인 필요
firstError.field가 nil인 경우 SignupField(rawValue: "")가 호출되어 .unknown("")이 됩니다. 이는 "필드 정보가 없음"과 "알 수 없는 빈 필드"가 구분되지 않는 문제가 있습니다.
🔧 수정 제안
if let firstError = errors.first {
- self.field = SignupField(rawValue: firstError.field ?? "")
+ self.field = firstError.field.flatMap { $0.isEmpty ? nil : SignupField(rawValue: $0) }
self.fieldMessage = firstError.message
} else {🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift` around lines
40 - 42, The code currently constructs SignupField(rawValue: firstError.field ??
"") which conflates a missing field (nil) with an empty/unknown raw value;
change the logic to explicitly check firstError.field: if firstError.field is
nil then assign a distinct sentinel (e.g., set self.field to nil if field is
optional or to a dedicated enum case like .missingField), otherwise attempt
SignupField(rawValue: fieldRaw) and fall back to an explicit unknown case for
unparsable values; keep assigning self.fieldMessage = firstError.message. Ensure
you update code paths referencing field to handle the new sentinel/optional
case.
| func test_signup_실제API호출() async throws { | ||
| // Given | ||
| print("========== 테스트 시작 ==========") | ||
| let request = SignupRequest( | ||
| fcmToken: "test-fcm-token-12345", | ||
| deviceModel: "iPhone", | ||
| deviceOs: "iOS", | ||
| deviceOsVersion: "17.0", | ||
| appVersion: "1.0.0" | ||
| ) | ||
| print("Request 생성 완료") | ||
|
|
||
| // When | ||
| print("API 호출 시작...") | ||
| let result = await sut.signup(request: request) | ||
| print("API 호출 완료") | ||
|
|
||
| // Then | ||
| switch result { | ||
| case .success(let response): | ||
| print("========== 성공 ==========") | ||
| print("UUID: \(response.uuid)") | ||
| print("AccessToken: \(response.accessToken)") | ||
| print("Nickname: \(response.nickname)") | ||
| print("==============================") | ||
| XCTAssertFalse(response.uuid.isEmpty, "UUID가 비어있음") | ||
| XCTAssertFalse(response.accessToken.isEmpty, "AccessToken이 비어있음") | ||
|
|
||
| case .failure(let error): | ||
| print("========== API 에러 ==========") | ||
| print("Type: \(error.type)") | ||
| print("Message: \(error.message)") | ||
| if let field = error.field { | ||
| print("Field: \(field)") | ||
| } | ||
| if let fieldMessage = error.fieldMessage { | ||
| print("FieldMessage: \(fieldMessage)") | ||
| } | ||
| print("=================================") | ||
| XCTAssertFalse(error.message.isEmpty, "에러 메시지가 비어있음") | ||
|
|
||
| case .networkFailure(let error): | ||
| print("==========네트워크 에러 ==========") | ||
| print("Error: \(error.message)") | ||
| print("=====================================") | ||
| XCTFail("네트워크 연결 실패: \(error.message)") | ||
| } | ||
|
|
||
| print("========== 테스트 종료 ==========") | ||
| } |
There was a problem hiding this comment.
실제 API 호출 테스트는 통합 테스트로 분리 권장
현재 테스트는 실제 서버 API를 호출하고 있어 다음과 같은 문제가 발생할 수 있습니다:
- 불안정성: 네트워크 상태나 서버 상태에 따라 테스트 결과가 달라짐
- 속도 저하: CI/CD 파이프라인에서 테스트 시간 증가
- 부작용: 실제 사용자 데이터가 생성될 수 있음
단위 테스트에서는 MoyaProvider를 모킹하고, 실제 API 호출은 별도의 통합 테스트 스위트로 분리하는 것을 권장합니다.
💡 모킹을 활용한 단위 테스트 예시
import Moya
final class AuthServiceTests: XCTestCase {
var sut: AuthService!
override func setUp() {
super.setUp()
// 스텁 데이터를 반환하는 MoyaProvider 사용
let stubbingProvider = MoyaProvider<AuthAPI>(
stubClosure: MoyaProvider.immediatelyStub
)
sut = AuthService(provider: stubbingProvider)
}
func test_signup_성공시_응답_파싱() async throws {
// Given - stubbed response
// When
let result = await sut.signup(request: request)
// Then - assert parsed response
}
}🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Tests/AuthServiceTests.swift` around lines 27 - 76,
The test test_signup_실제API호출 in AuthServiceTests makes a real network call via
sut.signup which should be replaced with a unit-test using a mocked
MoyaProvider; update AuthServiceTests to inject a stubbed MoyaProvider (e.g.,
MoyaProvider<AuthAPI> with stubClosure: .immediatelyStub) into the AuthService
instance (sut) and change test_signup_실제API호출 to assert parsing/handling of the
stubbed response instead of calling the real API, moving the live-call test to a
separate integration test suite.
ChoiAnYong
left a comment
There was a problem hiding this comment.
고생하셨습니다!
궁금한 부분 질문 남겼습니다.
| func request<T: Decodable, E: APIErrorProtocol>( | ||
| _ target: Target, | ||
| errorType: E.Type | ||
| ) async -> NetworkResult<T, E> { |
There was a problem hiding this comment.
async throws를 통해서 error를 넘겨줄 수 있을 거 같은데
NetworkResult를 사용해서 하는 이유가 있을까요?
There was a problem hiding this comment.
async throws를 사용하면 나중에 사용하는쪽에서
func signup(request: SignupRequest) async throws -> SignupResponse
// 사용시
do {
let response = try await authService.signup(request: request)
} catch let error as SignupError {
// 타입 캐스팅 필요
} catch {
// 알 수 없는 에러
}
이렇게 캐스팅이 필요하고,
NetworkResult를 사용하면,
func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError>
// 사용 시
switch result {
case .success(let response): ...
case .failure(let error): ... // 이미 SignupError 타입
case .networkFailure(let error): ...
}
에러 타입이 명시적이라 Generic E로 컴파일 타임에 에러 타입 보장이 되어서 캐스팅도 필요없고, 코드가 짧아지는것이 장점이라고 생각했습니다.
There was a problem hiding this comment.
하나의 경우에는 이점이 있는 거 같은데 async let이나 병렬처리를 사용해서 여러 usecase를 불러오는 경우
ansync throws의 경우 하나의 do catch문으로 해결하면 되지만 NetworkResult를 사용하게 되면
코드가 더 복잡해지지 않을까요??
There was a problem hiding this comment.
그런 고려를 많이는 못해봤었네요. 좋은 의견 감사합니다.
병렬처리가 필요한 부분이 있다면 말씀하신 것도 방법이 될 수 있을 것 같습니다.
그런데 병렬처리가 된다고 했을때, 예를들어 2개의 api를 병렬처리했다고 했을때, Partial Success부분이 조금 어려울 수 있을 것 같습니다. 예를들어 홈 화면에서 유저 정보 + 추천 피드 + 알림 개수를 동시에 불러오는 경우에는, 알림 API 하나 실패했다고 홈 화면 전체를 에러 처리를 해야하는 부분이 될 수 있습니다.
실제 앱에서는 실패한 부분만 대체하고 나머지는 보여줘야 하는 부분이 있어야한다고하면, 지금 구조처럼 각각을 분리해서 사용하는 걸로 생각했습니다. 어떻게 생각하시나용?
There was a problem hiding this comment.
넵 좋습니다. 해당 방법으로 진행해보다가 추후에 복잡하다고 판단되면 다른 방법 다시 생각해보면 좋을 거 같습니다!
| public struct SignupRequest: Encodable { | ||
| public let fcmToken: String | ||
| public let deviceModel: String? | ||
| public let deviceOs: String? | ||
| public let deviceOsVersion: String? | ||
| public let appVersion: String? | ||
|
|
||
| public init( | ||
| fcmToken: String, | ||
| deviceModel: String? = nil, | ||
| deviceOs: String? = nil, | ||
| deviceOsVersion: String? = nil, | ||
| appVersion: String? = nil | ||
| ) { | ||
| self.fcmToken = fcmToken | ||
| self.deviceModel = deviceModel | ||
| self.deviceOs = deviceOs | ||
| self.deviceOsVersion = deviceOsVersion | ||
| self.appVersion = appVersion | ||
| } | ||
| } |
There was a problem hiding this comment.
Service 객체를 통해 Domain 모듈에서 직접 Request 객체를 생성해서 주입하는 형태로 진행되기 때문에
Request를 init까지 public으로 열고 있는걸까요?
There was a problem hiding this comment.
맞습니다! 제가 이해하기로는 Domain/Feature 모듈에서 Request 객체를 직접 생성해야 하기 때문에 Public으로 열어두었는데 다른 방법도 추천해주시는게 있을까요? public이 많으니 개인적으로는 어색하더라구요..
There was a problem hiding this comment.
Domain 레이어에서 Network 레이어의 Request 객체를 알고 있는 게 클린아키텍처 관점에서 맞는 방식인가요??
There was a problem hiding this comment.
말씀해주신 방향이 조금 더 좋을 것 같습니다. 아무래도 Entity를 직접 만들어서 사용하는게 모델이 너무 많아지다보니 직접 사용하는 것을 고려했었는데, 조금 모듈화를 한 의미가 없어지는 것 같기도 하네요..
개선한다면 Domain이 Network DTO를 모르게 Protocol로 추상화를 하고, Repository Pattern을 도입하여 Domain이 Network DTO를 모르도록 분리해보면 어떨까요 ?
Feature → Domain ← Data → Network
↑
Protocol만 정의
There was a problem hiding this comment.
Protocol로 추상화하는 방법도 있고, 매개변수로 값들만 넘겨주는 방식으로 진행해도 좋을 것 같습니다!
There was a problem hiding this comment.
그런데 지금 network쪽 모듈을 잘못 설계했다는 생각이들어서 구조 리팩토링을 진행했습니다.. network쪽 모듈에 코드를 다 넣었었는데 data레이어와 분리하고, domain레이어는 순수하게 유지하였습니다.
There was a problem hiding this comment.
넵 해당 사항 반영되고 로거 추가되면 머지해도 될 거 같아요
There was a problem hiding this comment.
로거 추가했는데 네트워크 모듈쪽에 추가하는걸로 이해했는데 맞을까요? 아니면 Core에 생각하셨나요 ? 🥲 우선 맞으시다면 저도 머지하겠습니다~!
|
|
||
| import Foundation | ||
|
|
||
|
|
|
그리고 CI 실패했는데 로그 보니까 Sendable 채택 안해서 그런 거 같습니다! |
|
프로젝트 Swift Version도 6인데, 왜 제가 컴파일할때는 관련 경고가 없었을까요..? 좀 찾아보니 Release 설정에 |
|
해당 부분은 한 번 알아봐야할 것 같아요 근데 아마 빌드할 때는 debug를 사용해서 Release 설정이랑은 무관할 거 같아요! |
|
CI 말씀하시는게 맞나요 ? CI가 Debug로 돌아가고 있나요 ? |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Project.swift`:
- Around line 32-42: Add the missing xcconfig files referenced by settings:
create "xcconfigs/Debug.xcconfig" and "xcconfigs/Release.xcconfig" and define
the BASE_URL build setting inside them (e.g. BASE_URL = https://api.example.com)
so the infoPlist .extendingDefault with "BASE_URL": .string("$(BASE_URL)")
resolves; also ensure NetworkConfiguration.swift’s fatalError path no longer
triggers by providing a valid BASE_URL in the xcconfigs or adding a fallback in
NetworkConfiguration (e.g., guard for BASE_URL before using fatalError) to
prevent runtime crashes during tests.
In `@Projects/Modules/Networks/Sources/Service/AuthService.swift`:
- Around line 17-22: The class AuthService currently declares `@unchecked`
Sendable while holding a non-Sendable MoyaProvider<AuthAPI>, causing potential
data races; update the implementation to provide proper concurrency isolation:
either make AuthService an actor (change "public final class AuthService" to
"public actor AuthService" and keep the provider as a non-Sendable stored
property) and keep AuthServiceProtocol as Sendable, or remove the Sendable
requirement from AuthServiceProtocol and ensure provider access is confined to a
single serial executor (e.g., an internal actor or DispatchQueue) inside methods
that call provider.request; locate usages of AuthService, AuthServiceProtocol
and MoyaProvider<AuthAPI> to update signatures and call sites accordingly so the
concurrency model is consistent.
🧹 Nitpick comments (6)
Projects/Domain/Sources/Model/Auth/SignupError.swift (1)
21-26: LGTM!타입 안전한 에러 처리를 위한 잘 설계된 enum입니다.
unknown케이스를 통해 확장성도 고려되어 있습니다.테스트 편의성을 위해
Equatable준수를 고려해볼 수 있습니다:♻️ 선택적 개선 제안
-public enum SignupError: Error, Sendable { +public enum SignupError: Error, Sendable, Equatable {Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift (1)
16-25: 중복 코드 제거 가능
SignupField(rawValue:)이니셜라이저가 이미 동일한 매핑 로직을 제공하고 있어, 이 클로저를 간소화할 수 있습니다.♻️ DRY 원칙 적용 제안
let field: SignupField = { guard let fieldString = errors.first?.field else { return .unknown("unknown") } - switch fieldString { - case "fcmToken": return .fcmToken - case "email": return .email - default: return .unknown(fieldString) - } + return SignupField(rawValue: fieldString) }()Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (2)
59-97:request와requestPlain간 코드 중복두 메서드가 거의 동일한 로직을 공유하고 있어, 내부 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.
♻️ 공통 로직 추출 제안
private func performRequest<T: Decodable & Sendable, E: Error & Sendable>( _ target: Target, decoder: (Data) throws -> T, errorMapper: `@escaping` `@Sendable` (String, String, [ErrorResponse.ErrorDetail]) -> E ) async -> NetworkResult<T, E> { await withCheckedContinuation { continuation in self.request(target) { result in switch result { case .success(let response): do { let baseResponse = try JSONDecoder().decode( BaseResponse<T>.self, from: response.data ) if baseResponse.code == Self.successCode { guard let data = baseResponse.data else { continuation.resume(returning: .networkFailure(.decodingFailed)) return } continuation.resume(returning: .success(data)) } else { let errorResponse = try JSONDecoder().decode( ErrorResponse.self, from: response.data ) let apiError = errorMapper( errorResponse.code ?? "", errorResponse.message ?? "", errorResponse.errors ?? [] ) continuation.resume(returning: .failure(apiError)) } } catch { continuation.resume(returning: .networkFailure(.decodingFailed)) } case .failure(let moyaError): let networkError = Self.mapMoyaError(moyaError) continuation.resume(returning: .networkFailure(networkError)) } } } }
99-107: MoyaError 매핑 확장 고려현재
NSURLErrorDomain만connectionFailed로 매핑되고 나머지는 모두unknown으로 처리됩니다. 타임아웃, 인증서 오류 등 추가 케이스를 명시적으로 처리하면 디버깅에 유용할 수 있습니다.♻️ 추가 에러 케이스 매핑 예시
private static func mapMoyaError(_ error: MoyaError) -> NetworkError { switch error { case .underlying(let nsError as NSError, _) where nsError.domain == NSURLErrorDomain: + switch nsError.code { + case NSURLErrorTimedOut: + return .timeout + case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost: + return .connectionFailed + default: + return .connectionFailed + } - return .connectionFailed default: return .unknown(error.localizedDescription) } }
NetworkError에.timeout케이스 추가가 필요합니다.Projects/Data/Sources/Repository/Auth/AuthRepository.swift (2)
20-31: 변환 로직을AuthTransform확장으로 일원화하면 중복을 줄일 수 있습니다.
이미SignupRequest(from:)및SignupResult(from:)가 추가되어 있으니 Line 20-31에서 활용하면 매핑 중복을 피할 수 있습니다.♻️ 리팩터 예시
- let request = SignupRequest(fcmToken: info.fcmToken) + let request = SignupRequest(from: info) ... - let signupResult = SignupResult( - uuid: response.uuid, - accessToken: response.accessToken, - nickname: response.nickname - ) + let signupResult = SignupResult(from: response)
13-14:@unchecked Sendable사용 재평가 필요
AuthServiceProtocol은 이미Sendable로 정의되어 있으므로, 원래 제안("AuthServiceProtocol을 Sendable로 만들어라")은 불필요합니다. 다만AuthRepository가@unchecked Sendable을 사용하는 이유는 의존성인AuthService가 외부 라이브러리MoyaProvider로 인해@unchecked를 사용하기 때문입니다.
AuthRepository자체는 불변 상태만 가지고 있고 순수하게authService에 위임하므로,@unchecked를 제거하고 순수Sendable로 변경할 수 있는지 검토해 주세요. 다만 현재 구조에서 데이터 경합 위험은 없어 보입니다.
| infoPlist: .extendingDefault(with: [ | ||
| "BASE_URL": .string("$(BASE_URL)") | ||
| ]), | ||
| sources: ["Tests/**"], | ||
| dependencies: [ | ||
| .target(name: "Networks") | ||
| ], | ||
| settings: .settings(configurations: [ | ||
| .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfigs/Debug.xcconfig")), | ||
| .release(name: "Release", xcconfig: .relativeToRoot("xcconfigs/Release.xcconfig")) | ||
| ]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for BASE_URL definition in xcconfig files
echo "=== Searching for BASE_URL in xcconfig files ==="
rg -n "BASE_URL" xcconfigs -g "*.xcconfig"
echo -e "\n=== Listing all xcconfig files ==="
fd -t f -e xcconfig xcconfigs
echo -e "\n=== Content of xcconfig files ==="
fd -t f -e xcconfig xcconfigs -x cat -n {}Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 194
🏁 Script executed:
#!/bin/bash
# Check if xcconfigs directory exists
echo "=== Checking xcconfigs directory ==="
ls -la xcconfigs 2>&1 || echo "Directory does not exist"
echo -e "\n=== Searching for any xcconfig files in entire repo ==="
find . -name "*.xcconfig" -type f 2>/dev/null
echo -e "\n=== Checking project structure around Projects/Modules/Networks ==="
ls -la Projects/Modules/Networks/ 2>&1 | head -30
echo -e "\n=== Searching for BASE_URL references in entire project ==="
rg -n "BASE_URL" . -g "!.git" | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1364
xcconfig 파일 누락으로 인한 빌드 실패 위험 - 즉시 수정 필요
39-42줄에서 참조하는 xcconfigs/Debug.xcconfig와 xcconfigs/Release.xcconfig 파일이 존재하지 않습니다. 33줄의 $(BASE_URL) 빌드 변수는 해석되지 않아 테스트 빌드가 실패하거나, NetworkConfiguration.swift의 fatalError(18줄)로 인해 런타임에 충돌합니다. 누락된 xcconfig 파일들을 추가하고 BASE_URL을 정의해야 합니다.
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Project.swift` around lines 32 - 42, Add the
missing xcconfig files referenced by settings: create "xcconfigs/Debug.xcconfig"
and "xcconfigs/Release.xcconfig" and define the BASE_URL build setting inside
them (e.g. BASE_URL = https://api.example.com) so the infoPlist
.extendingDefault with "BASE_URL": .string("$(BASE_URL)") resolves; also ensure
NetworkConfiguration.swift’s fatalError path no longer triggers by providing a
valid BASE_URL in the xcconfigs or adding a fallback in NetworkConfiguration
(e.g., guard for BASE_URL before using fatalError) to prevent runtime crashes
during tests.
| public final class AuthService: AuthServiceProtocol, @unchecked Sendable { | ||
| private let provider: MoyaProvider<AuthAPI> | ||
|
|
||
| public init(provider: MoyaProvider<AuthAPI> = MoyaProvider<AuthAPI>()) { | ||
| self.provider = provider | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Moya MoyaProvider Sendable Swift 6 concurrency support
💡 Result:
Short answer: Moya’s MoyaProvider is not annotated as Sendable by the library and therefore will trigger Sendable checks under Swift 6’s stricter concurrency model. The Moya codebase still exposes MoyaProvider as a reference-type provider/AnyObject-based API (not @Sendable), so you should treat it as non‑Sendable or use an explicit workaround (actor/flow-confinement or @unchecked Sendable) until the library authors add Sendable compatibility. [1][2]
Practical options
- Confine MoyaProvider usage to a single actor or main/serial queue (recommended). This avoids Sendable violations without changing library code. [0]
- If you must share a provider across tasks/actors and you are sure it’s safe, declare a small wrapper or extension: extension MoyaProvider:
@uncheckedSendable {} — but understand this silences the compiler check (unsafe unless you audited thread-safety). [3][0]
Sources
- Moya repository / docs (shows MoyaProvider is a class/AnyObject-based provider; no Sendable annotations). [1]
- Swift Concurrency / Swift 6 Sendable guidance (explains why libraries must mark types Sendable for Swift 6). [0]
- Swift community discussion on marking library types unchecked Sendable (tradeoffs of
@uncheckedSendable). [3]
Would you like a short code example for (a) an actor-wrapped provider or (b) an @unchecked Sendable wrapper?
🏁 Script executed:
# Search for CI configuration and any CI failure context
git ls-files | grep -E '\.(yml|yaml|swift)' | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1059
🏁 Script executed:
# Look for MoyaProvider usage and Sendable related comments in the codebase
rg -i "moya|sendable" --type swift -A 3 -B 3 | head -100Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 8042
🏁 Script executed:
# Check the actual AuthService file to understand the full context
cat -n Projects/Modules/Networks/Sources/Service/AuthService.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1023
@unchecked Sendable 사용 - Moya 라이브러리 Sendable 미지원으로 인한 동시성 안전 우려
Moya 15.0.0의 MoyaProvider는 Sendable을 지원하지 않으며, 내부적으로 가변 상태를 포함합니다. AuthServiceProtocol이 Sendable을 선언하므로 구현 클래스도 이를 만족해야 하는데, @unchecked Sendable은 컴파일러 검사를 우회하는 임시 해결책입니다. 이로 인해 async/await 경계에서 데이터 경쟁 상태(data race)가 발생할 수 있습니다.
권장 해결 방안:
- 가장 권장:
AuthService를 단일 actor로 격리하거나 전담 스레드/큐에 한정하여 provider 접근을 제어합니다. - 프로토콜 재설계:
AuthServiceProtocol의Sendable요구사항 제거를 검토하고, actor 기반 구현으로 전환합니다.
♻️ Actor 기반 구현 예시
public actor AuthService: AuthServiceProtocol {
private let provider: MoyaProvider<AuthAPI>
public init(provider: MoyaProvider<AuthAPI> = MoyaProvider<AuthAPI>()) {
self.provider = provider
}
public func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError> {
await provider.request(.signup(request: request), errorMapper: SignupError.init)
}
}
public protocol AuthServiceProtocol: Sendable {
func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError>
}🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Service/AuthService.swift` around lines 17
- 22, The class AuthService currently declares `@unchecked` Sendable while holding
a non-Sendable MoyaProvider<AuthAPI>, causing potential data races; update the
implementation to provide proper concurrency isolation: either make AuthService
an actor (change "public final class AuthService" to "public actor AuthService"
and keep the provider as a non-Sendable stored property) and keep
AuthServiceProtocol as Sendable, or remove the Sendable requirement from
AuthServiceProtocol and ensure provider access is confined to a single serial
executor (e.g., an internal actor or DispatchQueue) inside methods that call
provider.request; locate usages of AuthService, AuthServiceProtocol and
MoyaProvider<AuthAPI> to update signatures and call sites accordingly so the
concurrency model is consistent.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Sources/Extensions/MoyaProvider`+Async.swift:
- Around line 15-62: The request(_:errorMapper:) async extension captures self
(MoyaProvider) inside a `@Sendable` withCheckedContinuation but MoyaProvider in
Moya 15 isn't Sendable under Swift 6, causing strict-concurrency compile errors;
fix by adding `@preconcurrency` import Moya at the top of this file (before any
use of Moya types) to opt-out of Sendable checking for the Moya module, or
alternatively ensure callers use an `@unchecked` Sendable wrapper when passing
MoyaProvider into async contexts; target the request<T: Decodable & Sendable, E:
Error & Sendable> function and its use of withCheckedContinuation as the
locations to address.
🧹 Nitpick comments (2)
Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift (1)
49-62:parameters.description은 JSON 형식이 아닌 Swift Dictionary 기본 출력 형식을 반환합니다.
requestParameters케이스에서parameters.description을 사용하면["key": value]형태의 Swift Dictionary 문자열이 출력됩니다.requestJSONEncodable케이스와 일관성을 맞추려면JSONSerialization을 사용하여 JSON 문자열로 변환하는 것이 좋습니다.♻️ 개선 제안
private static func requestBody(from task: Moya.Task) -> String? { switch task { case .requestParameters(let parameters, _): - return parameters.description + if let data = try? JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted), + let string = String(data: data, encoding: .utf8) { + return string + } + return parameters.description case .requestJSONEncodable(let encodable): if let data = try? JSONEncoder().encode(AnyEncodable(encodable)), let string = String(data: data, encoding: .utf8) { return string } return nil default: return nil } }Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (1)
64-107:request와requestPlain의 디코딩 로직 중복성공/에러 디코딩과
NetworkResult매핑이 두 함수에 거의 동일하게 복제돼 유지보수 비용이 커질 수 있습니다. 공통 helper(예:decodeResponse<T>또는handleResponse)로 추출해 DRY하게 유지하는 것을 추천합니다.
| func request<T: Decodable & Sendable, E: Error & Sendable>( | ||
| _ target: Target, | ||
| errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E | ||
| ) async -> NetworkResult<T, E> { | ||
| NetworkLogger.logRequest(target) | ||
|
|
||
| return await withCheckedContinuation { continuation in | ||
| self.request(target) { result in | ||
| switch result { | ||
| case .success(let response): | ||
| NetworkLogger.logResponse(response) | ||
|
|
||
| do { | ||
| let baseResponse = try JSONDecoder().decode( | ||
| BaseResponse<T>.self, | ||
| from: response.data | ||
| ) | ||
|
|
||
| if baseResponse.code == Self.successCode { | ||
| guard let data = baseResponse.data else { | ||
| continuation.resume(returning: .networkFailure(.decodingFailed)) | ||
| return | ||
| } | ||
| continuation.resume(returning: .success(data)) | ||
| } else { | ||
| let errorResponse = try JSONDecoder().decode( | ||
| ErrorResponse.self, | ||
| from: response.data | ||
| ) | ||
| let apiError = errorMapper( | ||
| errorResponse.code ?? "", | ||
| errorResponse.message ?? "", | ||
| errorResponse.errors ?? [] | ||
| ) | ||
| continuation.resume(returning: .failure(apiError)) | ||
| } | ||
| } catch { | ||
| continuation.resume(returning: .networkFailure(.decodingFailed)) | ||
| } | ||
|
|
||
| case .failure(let moyaError): | ||
| NetworkLogger.logError(moyaError) | ||
| let networkError = Self.mapMoyaError(moyaError) | ||
| continuation.resume(returning: .networkFailure(networkError)) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the file structure and related files
find . -type f -name "*.swift" | grep -i moya | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 146
🏁 Script executed:
# Look for the MoyaProvider definition and its Sendable conformance
rg -l "MoyaProvider" --type swift | head -10Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 204
🏁 Script executed:
# Check the specific file structure
fd -t f "MoyaProvider+Async.swift" 2>/dev/nullRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
🏁 Script executed:
# Look for Sendable conformance in the codebase
rg "extension MoyaProvider|class MoyaProvider|actor MoyaProvider" --type swift -A 5Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 756
🏁 Script executed:
# Read the full MoyaProvider+Async.swift file
cat -n Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 5542
🏁 Script executed:
# Check if there are any `@unchecked` Sendable or Sendable conformance declarations
rg "MoyaProvider.*Sendable|@unchecked.*Sendable" --type swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 349
🏁 Script executed:
# Look for import statements and package configuration
cat -n Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 726
🏁 Script executed:
# Find project configuration files to check Swift version and strict concurrency settings
fd -t f "Package.swift|project.yml|.swift-version" | xargs cat 2>/dev/null | head -100Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 983
🏁 Script executed:
# Check for Moya import and version pinning
rg "import Moya|preconcurrency" --type swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 372
🏁 Script executed:
# Check if there are any Xcode build settings or strict concurrency configurations
fd -t f "*.xcconfig|project.yml|Tuist" | xargs grep -l "SWIFT_STRICT_CONCURRENCY\|strict" 2>/dev/null || echo "No strict concurrency configs found"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 435
🏁 Script executed:
# Look for MoyaProvider in Moya library or check if there's any additional setup
rg "extension MoyaProvider" --type swift -B 2 -A 10Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1446
🏁 Script executed:
# Check if the Moya library files are present or check for any `@preconcurrency` imports
rg "@preconcurrency" --type swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
🏁 Script executed:
# Check Moya version in dependencies and look for any swift-concurrency related files
rg "Moya" Projects/Modules/Networks/Project.swift 2>/dev/null || \
rg "Moya" Projects/Modules/Networks/ --type swift | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1660
🏁 Script executed:
# Search for any NetworkError, NetworkResult definitions to understand error handling
fd -t f "NetworkError.swift|NetworkResult.swift"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 191
🏁 Script executed:
# Check NetworkError and NetworkResult definitions
cat -n Projects/Modules/Networks/Sources/Base/NetworkError.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 831
🏁 Script executed:
# Check NetworkResult definition
cat -n Projects/Modules/Networks/Sources/Base/NetworkResult.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 500
🏁 Script executed:
# Check Moya version compatibility with Swift 6 concurrency
web_search
Moya 15.0.0 Swift 6 Sendable support compatibilityRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 168
🏁 Script executed:
# Check if MoyaProvider is thread-safe or has Sendable conformance in documentation
web_search
MoyaProvider Sendable Swift 6 concurrencyRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 176
🏁 Script executed:
# Look at the AuthService to see how MoyaProvider is used with `@unchecked` Sendable
cat -n Projects/Modules/Networks/Sources/Service/AuthService.swift | head -30Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1023
🌐 Web query:
Moya 15.0.0 Swift 6 Sendable concurrency support
💡 Result:
Short answer: Moya 15.0.0 does not ship with explicit Swift 6 "Sendable"-first annotations/guarantees. Moya 15 was released for Swift 5.x compatibility (README maps Moya 15 → Swift ≥5.2) and the project does not advertise a Swift‑6 / full Sendable migration in the 15.0.0 docs/releases—so you should expect to need local fixes (e.g. adding @unchecked Sendable extensions, adding explicit Sendable conformances, or small wrapper/adapters) when compiling with Swift 6 strict concurrency. [1][2][3]
What to do:
- If you compile in Swift 6 mode, add targeted Sendable/@unchecked Sendable annotations in your app or submit small PRs to Moya for missing conformances. [2]
- Alternatively compile in Swift 5 language mode / opt out of strict Swift 6 concurrency until you migrate. [2]
- Check the Moya repo for any community PRs or later releases if you need upstream changes. [1][3]
Sources:
- Moya repository / README (Moya 15.0.0 guidance). [1]
- Swift Concurrency adoption / Sendable guidance (why libraries must add annotations for Swift 6). [2]
- Moya releases page (15.0.0/alpha notes). [3]
Would you like me to scan the Moya repo now for specific files/types you need marked Sendable and suggest exact patches?
🌐 Web query:
withCheckedContinuation Sendable closure self capture Swift 6
💡 Result:
Short answer: In Swift 6 Task/async closures are @Sendable by default, so using withCheckedContinuation (or Task) inside a method can trigger "capture of 'self' with non‑sendable type" when the closure implicitly captures self. Fixes: capture only Sendable things (capture the actor property instead of self), copy needed values into local Sendable variables, make the captured type Sendable (or use an actor/immutable types), or explicitly avoid sending non‑Sendable state by using weak captures or the new sending/move semantics. Examples and guidance: capture the actor/property directly (Task { [worker] in await worker.doWork() }), or do let value = self.foo; Task { [value] in ... } rather than capturing self. References: Dev Solve (fixes and capture-list advice), Swift6 Sendable closure discussion, and notes about withCheckedContinuation/actor interaction. [1][2][3]
References
- Fixing 'Capture of self with non-sendable type' in Swift 6 — Dev Solve. [1]
- “Taming 'self' in
@SendableClosures” (Swift 6 capture-list and sending closure patterns). [2] - Sendable closures & withCheckedContinuation / actor notes. [3]
Swift 6 strict concurrency 환경에서 Sendable 규칙 위반 가능성
withCheckedContinuation 클로저는 @Sendable이며, 여기서 self(MoyaProvider)가 capture되는데, Moya 15.0.0은 Swift 6 Sendable 마이그레이션을 완료하지 않아 MoyaProvider가 Sendable을 준수하지 않습니다. 따라서 Swift 6 strict concurrency 모드에서 컴파일 오류/경고가 발생할 수 있습니다.
권장 해결책: import Moya 앞에 @preconcurrency import Moya를 추가하거나, 현재 AuthService 패턴처럼 호출자 레벨에서 @unchecked Sendable을 명시적으로 적용합니다.
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Extensions/MoyaProvider`+Async.swift around
lines 15 - 62, The request(_:errorMapper:) async extension captures self
(MoyaProvider) inside a `@Sendable` withCheckedContinuation but MoyaProvider in
Moya 15 isn't Sendable under Swift 6, causing strict-concurrency compile errors;
fix by adding `@preconcurrency` import Moya at the top of this file (before any
use of Moya types) to opt-out of Sendable checking for the Moya module, or
alternatively ensure callers use an `@unchecked` Sendable wrapper when passing
MoyaProvider into async contexts; target the request<T: Decodable & Sendable, E:
Error & Sendable> function and its use of withCheckedContinuation as the
locations to address.
🔗 연결된 이슈
📄 작업 내용
💻 주요 코드 설명
0. 설계 시 신경 쓴 점
에러 코드와 필드를 String이 아닌 Enum으로 접근하도록 설계했습니다.
이렇게 하면:
1. 서버 응답 구조 대응
서버의 공통 응답 형식을 분석하여
BaseResponse와ErrorResponse로 대응했습니다.2. API별 에러를 Enum으로 타입 안전하게 처리
각 API마다 발생할 수 있는 에러를 enum으로 정의하여 컴파일 타임에 안전하게 처리할 수 있습니다.
3. 에러 발생 시 대응 방법
서버에서 errors 배열로 여러 필드 에러가 올 수 있지만, 첫 번째 에러를 기준으로 매칭하여 처리합니다.
4. 테스트 코드
실제 API를 호출하는 테스트 코드로 네트워크 모듈의 동작을 검증할 수 있습니다.
5. 확장성
👀 기타 더 이야기해볼 점
[SignupField: String] 형태로 확장 가능합니다.
서버 데이터에 영향을 줄 수 있으므로 필요할 때만 실행하는 것이 좋을 것 같습니다. 아니면 지금 구조 외에 다르게 하면 좋을지에 대해서도 의견 주시면 좋을 것 같습니다.
📄 작업 내용
Summary by CodeRabbit
릴리스 노트
New Features
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.