iOS ๊ฐ๋ฐ์์ ์น ์ ์๊ธฐ - ๋ฏ์ ์ฝ๋์์ ์ต์ํ ๊ตฌ์กฐ๋ฅผ ๋ฐ๊ฒฌํ๊ธฐ๊น์ง
- #๋ชจ๋ฐ์ผ
- #iOS
- #Web
- #Frontend
์๋ ํ์ธ์. Fromm ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ๋ฐ์ง๋ฏผ์ ๋๋ค.
์ฌํด ์ด iOS ๊ฐ๋ฐ์์ ์น ํ๋ก ํธ์๋ ๊ฐ๋ฐ๋ก ์ ๋ฌด๋ฅผ ์ ํํ๊ฒ ๋๋ฉด์ ์๋ก์ด ํ๋ก์ ํธ๋ฅผ ์ ํ๊ฒ ๋์์ต๋๋ค. ์น ๊ฐ๋ฐ์ ์์ํ๊ธฐ ์ ์๋ ์ธ์ด์ ํ๋ ์์ํฌ๋ฟ ์๋๋ผ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ฑํ๋ ์ํคํ ์ฒ ์ญ์ iOS์๋ ์๋นํ ๋ค๋ฅผ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ต๋๋ค. Swift์์ TypeScript๋ก, UIKit์์ React๋ก ๊ฐ๋ฐ ํ๊ฒฝ์ด ๋ฐ๋์๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด์ ์ต์ํ๋ ์ค๊ณ ๋ฐฉ์๋ ๋๋ถ๋ถ ์๋กญ๊ฒ ์ตํ์ผ ํ ๊ฒ์ด๋ผ ์์ํ์ต๋๋ค.
ํ์ง๋ง ์ค์ ํ๋ก์ ํธ์ ์ฝ๋๋ฒ ์ด์ค๋ฅผ ๋ถ์ํ๋ฉด์ ์์๊ณผ๋ ๋ค๋ฅธ ์ ์ ๋ฐ๊ฒฌํ์ต๋๋ค.
// queries/arti/useGetArtiTrial/index.ts
const useGetArtiTrial = () =>
useSuspendedQuery({
queryKey: ["getArtiTrial"],
fn: (signal) => getArtiTrial(signal).then(ArtiTrialVo.from),
});
์ฒ์์๋ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ React์ Custom Hook ์ ๋๋ก ์ดํดํ์ต๋๋ค. ํ์ง๋ง ๊ตฌํ์ ๋ฐ๋ผ๊ฐ๋ค ๋ณด๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ณ , ํ๋ก ํธ์๋์์ ์ฌ์ฉํ ๊ฐ์ฒด๋ก ๋ณํํ ๋ค, ์ปดํฌ๋ํธ์ ์ ๋ฌํ๋ ํ๋ฆ์ด iOS์์ ์ต์ํ๋ ๊ตฌ์กฐ์ ์๋นํ ์ ์ฌํ๋ค๋ ์ ์ ํ์ธํ ์ ์์์ต๋๋ค.
์ดํ ํ๋ก์ ํธ์ ๋ค๋ฅธ ์์ญ๋ค๋ ํจ๊ป ์ดํด๋ณด๋ฉด์ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ๊ณ , ๋ ์ด์ด๋ฅผ ๋ ๋ฆฝ์ํค๋ฉฐ, ์์กด์ฑ์ ๊ด๋ฆฌํ๋ ๋ฐฉ์ ์ญ์ ๋ชจ๋ฐ์ผ ๊ฐ๋ฐ์์ ์ฌ์ฉํ๋ ์ค๊ณ ์์น๊ณผ ๊ณตํต์ ์ด ์๋ค๋ ์ ์ ํ์ธํ ์ ์์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ iOS ๊ฐ๋ฐ ๊ฒฝํ์ ๊ธฐ์ค์ผ๋ก ์น ํ๋ก ํธ์๋ ํ๋ก์ ํธ๋ฅผ ๋ถ์ํ๋ฉด์ ๋ฐ๊ฒฌํ ๊ตฌ์กฐ์ ์ ์ฌ์ฑ๊ณผ ๋ ํ๋ซํผ์ ์ฐจ์ด์ ์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
๋น๊ต์ ๊ธฐ์ค์ด ๋ iOS ๊ตฌ์กฐ
์น ํ๋ก์ ํธ๋ฅผ ์ดํดํ๊ธฐ ์ํด ๋จผ์ ์ ๊ฐ ์ต์ํ๋ iOS ํ๋ก์ ํธ ๊ตฌ์กฐ๋ถํฐ ๊ฐ๋จํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
Fromm iOS ์ฑ์ Clean Architecture๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Presentation, Domain, Data ์ธ ๊ฐ์ ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
- Presentation: ํ๋ฉด๊ณผ ์ํ๋ฅผ ๊ด๋ฆฌ
- Domain: UseCase๋ฅผ ํตํด ๋น์ฆ๋์ค ๋ก์ง์ ์ํ
- Data: Repository๋ฅผ ํตํด API์ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌ
ํต์ฌ์ ๊ณ์ธต ๊ฐ ์ฑ ์์ ๋ถ๋ฆฌํ๊ณ , ์์กด์ฑ์ ํ ๋ฐฉํฅ์ผ๋ก ์ ์งํ๋ ๊ฒ์ ๋๋ค. ์๋ฅผ ๋ค์ด ViewModel์ UseCase๋ฅผ ํธ์ถํ๊ณ , UseCase๋ Repository๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์กฐํํฉ๋๋ค.
final class FanProfileEditViewModel: BaseViewModel {
@Inject private var loadFanProfileUseCase: LoadFanProfileUseCase
@Inject private var updateFanProfileUseCase: UpdateFanProfileUseCase
}
์ด ๊ตฌ์กฐ๋ฅผ ๊ธฐ์ค์ผ๋ก ์น ํ๋ก์ ํธ์ ๋ฐ์ดํฐ ํ๋ฆ์ ํ๋์ฉ ์ดํด๋ณด์์ต๋๋ค.
์น ํ๋ก์ ํธ๋ฅผ ์ฒ์ ๋ดค์ ๋
์ฒ์ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ๋ดค์ ๋๋ ๊ฐ ๋๋ ํฐ๋ฆฌ์ ์ญํ ์ ๋ฐ๋ก ์ดํดํ๊ธฐ ์ด๋ ค์ ์ต๋๋ค.
src/
โโโ app/
โโโ api/
โโโ vo/
โโโ queries/
โโโ hooks/
โโโ components/
api/์ components/์ ์ญํ ์ ๋น๊ต์ ์ง๊ด์ ์ด์์ง๋ง queries/, vo/, hooks/๋ ๊ฐ๊ฐ ์ด๋ค ๊ณ์ธต์ ๋ด๋นํ๋์ง ๋ฐ๋ก ์ดํดํ๊ธฐ ์ด๋ ค์ ์ต๋๋ค.
์ฒ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ถ์ธกํ์ต๋๋ค.
queries/โ Repositoryvo/โ DTOhooks/โ UseCase
ํ์ง๋ง ์ค์ ๊ตฌํ์ ์ ์์๊ณผ ๋ฌ๋์ต๋๋ค. ๋๋ ํฐ๋ฆฌ ์ด๋ฆ๋ง์ผ๋ก ๊ตฌ์กฐ๋ฅผ ์ดํดํ๊ธฐ๋ณด๋ค๋ API ์์ฒญ๋ถํฐ ํ๋ฉด ๋ ๋๋ง๊น์ง ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์ ๋ฌ๋๋์ง๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉด์ ๊ฐ ๊ณ์ธต์ ์ญํ ์ ํ์ธํด ๋ณด๊ธฐ๋ก ํ์ต๋๋ค.
๋ฐ์ดํฐ ํ๋ฆ์ ๋ฐ๋ผ๊ฐ๋ฉฐ ๋ณธ ๊ตฌ์กฐ
์ํฐ์คํธ ์ ๋ณด๋ฅผ ์กฐํํ๋ ๊ธฐ๋ฅ์ ๊ธฐ์ค์ผ๋ก ์ดํด๋ณด์์ต๋๋ค.
1. API Layer
export const getArti = (artiPath: string, signal?: AbortSignal) =>
nativeFetch<GetArtiResponse>({
url: `/arti/${artiPath}`,
signal,
});
๊ฐ์ฅ ์๋์์๋ API์ ํต์ ํ์ฌ ์๋ฒ ์๋ต์ ๊ฐ์ ธ์ต๋๋ค. ์ด ์ญํ ์ iOS์ Network Data Source์ ์ ์ฌํ์ต๋๋ค.
2. VO Layer
class ArtiVo {
readonly id: string;
readonly name: string;
static from(response: GetArtiTrial): ArtiVo {
return new ArtiVo(response);
}
private constructor(response: GetArtiTrial) {
this.id = response.id;
this.name = response.name;
}
}
์ฌ๊ธฐ์๋ ์๋ฒ ์๋ต์ ํ๋ก ํธ์๋์์ ์ฌ์ฉํ ๊ฐ์ฒด๋ก ๋ณํํฉ๋๋ค.
TypeScript๋ ๊ตฌ์กฐ์ ํ์ ์์คํ (Structural Typing)์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ๊ตฌ์กฐ๊ฐ ๊ฐ์ผ๋ฉด ๊ฐ์ ํ์ ์ผ๋ก ๊ฐ์ฃผํฉ๋๋ค. ์ด ๋๋ฌธ์ ์๋ฒ ์๋ต ํ์ ๊ณผ ๋ด๋ถ ๋ชจ๋ธ์ด ์ฐ์ฐํ ๊ฐ์ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง ๊ฒฝ์ฐ ๊ฒฝ๊ณ๊ฐ ํ๋ฆฟํด์ง ์ ์์ต๋๋ค.
VO.from()๊ณผ private constructor ์กฐํฉ์ ์ด ๊ฒฝ๊ณ๋ฅผ ์ฝ๋ ๋ ๋ฒจ์์ ๋ช
์์ ์ผ๋ก ๊ฐ์ ํ๋ ๋ฐฉ์์
๋๋ค. ๋ฐ๋์ from()์ ํตํด์๋ง ์์ฑํ ์ ์๋๋ก ์ ํํจ์ผ๋ก์จ, Structural Typing ํ๊ฒฝ์์๋ ์๋ฒ ์๋ต๊ณผ ํ๋ก ํธ์๋ ๋ด๋ถ ๋ชจ๋ธ์ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค. iOS์์๋ Mapper์ Domain Model์ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋๋ฐ, ์น ํ๋ก์ ํธ์์๋ VO๊ฐ ์ด๋ฌํ ์ญํ ์ ํจ๊ป ์ํํ๊ณ ์์์ต๋๋ค.
3. Query Layer
const useGetArtiTrial = () =>
useSuspendedQuery({
queryKey: ["getArtiTrial"],
fn: (signal) =>
getArtiTrial(signal).then(ArtiTrialVo.from),
});
Query๋ API๋ฅผ ํธ์ถํ๊ณ , ์๋ต์ VO๋ก ๋ณํํ ๋ค, ์ปดํฌ๋ํธ๊ฐ ์ฌ์ฉํ ์ ์๋ ํํ๋ก ์ ๊ณตํฉ๋๋ค. ์ด๋ฌํ ํ๋ฆ์ ํ๋์ ๊ธฐ๋ฅ ๋จ์๋ฅผ ์บก์ํํ๋ค๋ ์ ์์ iOS์ UseCase์ ์ญํ ์ด ์ผ๋ถ ์ ์ฌํ์ต๋๋ค.
๋ค๋ง ๋ ๊ตฌ์กฐ๋ฅผ ๋์ผํ ๊ฐ๋ ์ผ๋ก ๋ณผ ์๋ ์์ต๋๋ค. iOS์ UseCase๋ ์ผ๋ฐ์ ์ผ๋ก ์ํ๋ฅผ ๋ณด๊ดํ์ง ์๋ ๋น์ฆ๋์ค ๋ก์ง์ ๋๋ค. ๋ฐ๋ฉด React Query ๊ธฐ๋ฐ์ Query Layer๋ ์๋ฒ ์ํ๋ฅผ ์บ์ฑํ๊ณ , ์ฌ์์ฒญ์ ๊ด๋ฆฌํ๋ฉฐ, ๋ฐ์ดํฐ์ ์ ํจ ๊ธฐ๊ฐ๊น์ง ํจ๊ป ๊ด๋ฆฌํฉ๋๋ค.
์ฆ, Query Layer๋ UseCase์ ์ ์ฌํ ์ญํ ์ ์ํํ๋ฉด์๋ ์๋ฒ ์ํ ๊ด๋ฆฌ(Server State Management)๋ผ๋ ์น ํ๊ฒฝ์ ์ฑ ์์ ํจ๊ป ๊ฐ์ง๊ณ ์์์ต๋๋ค.
๊ตฌ์กฐ ๋น๊ต ์์ฝ
| ์ญํ | iOS | Web |
|---|---|---|
| ์ธ๋ถ ๋ฐ์ดํฐ ์กฐํ | Repository / Data Source | api/ |
| ๋ฐ์ดํฐ ๋ณํ | Mapper | VO.from() |
| ๋ด๋ถ ๋ฐ์ดํฐ ๋ชจ๋ธ | Domain Model | VO |
| ๊ธฐ๋ฅ ๋จ์ | UseCase | Query |
| ํ๋ฉด ์ํ | ViewModel | Custom Hook |
| UI | UIViewController / SwiftUI | Component |
๋ฌผ๋ก ์ด ํ๋ ๊ฐ๋ ์ ์ธ ๋์ ๊ด๊ณ์ผ ๋ฟ์ด๋ฉฐ, ๋ ๊ตฌ์กฐ๊ฐ ๋์ผํ ์ํคํ ์ฒ๋ผ๋ ์๋ฏธ๋ ์๋๋๋ค.
DI๋ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์กด์ฌํ๊ณ ์์๋ค
iOS์์๋ Protocol๊ณผ DI Container๋ฅผ ์ด์ฉํด ์์กด์ฑ์ ์ฃผ์ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
@Inject private var loadFanProfileUseCase: LoadFanProfileUseCase
๋ฐ๋ฉด ์น ํ๋ก์ ํธ์์๋ ๋๋ถ๋ถ ES Module์ ์ด์ฉํด ํ์ํ ๋ชจ๋์ ์ง์ ๊ฐ์ ธ์ต๋๋ค.
import { getArtiTrial } from "api/arti";
import { ArtiTrialVo } from "vo/arti/ArtiTrial.vo";
ES Module ์์ฒด๋ DI ๋ฉ์ปค๋์ฆ์ ์๋๋๋ค. ํ์ง๋ง ์์กด์ฑ์ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ๊ณ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๋ ค๋ ๋ชฉ์ ์ iOS์ ํฌ๊ฒ ๋ค๋ฅด์ง ์์์ต๋๋ค.
ํ ์คํธ ๋ฐฉ์์์๋ ์ฐจ์ด๊ฐ ์์์ต๋๋ค. iOS์์๋ Protocol ๊ธฐ๋ฐ Mock ๊ฐ์ฒด๋ฅผ ์ฃผ์ ํ๋ ๋ฐฉ์์ด ์ผ๋ฐ์ ์ด๋ผ๋ฉด, ์น์์๋ MSW(Mock Service Worker) ๋ฑ์ ์ด์ฉํด ๋คํธ์ํฌ ์์ฒด๋ฅผ ๋ชจํนํ๋ ๋ฐฉ์์ ๋ง์ด ์ฌ์ฉํ์ต๋๋ค. ์ ๊ทผ ๋ฐฉ์์ ๋ค๋ฅด์ง๋ง ์ธ๋ถ ํ๊ฒฝ๊ณผ์ ๊ฒฐํฉ์ ์ค์ฌ ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ํ๋ณดํ๋ค๋ ๋ชฉ์ ์ ๊ณตํต์ ์ด์์ต๋๋ค.
๋ง๋ฌด๋ฆฌ
ํ๋ก์ ํธ๋ฅผ ์ดํดํ๊ธฐ ์ํด ๋ฐ์ดํฐ์ ํ๋ฆ์ ๋ฐ๋ผ๊ฐ๋ค ๋ณด๋, ์ฒ์์๋ ๋ฏ์ค๊ฒ ๋ณด์๋ ๊ตฌ์กฐ๋ค์ด ํ๋์ฉ ์ต์ํ ์ญํ ๋ก ์ฝํ๊ธฐ ์์ํ์ต๋๋ค.
๋ฌผ๋ก React Query๋ฅผ UseCase๋ก, VO๋ฅผ Domain Model๋ก ๋จ์ํ ๋์์ํฌ ์๋ ์์ต๋๋ค. ๊ฐ ๊ธฐ์ ์ ์์ ์ด ์ํ ์ํ๊ณ์ ํน์ฑ๊ณผ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ์๋ก ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋ค๋ง ํ๋ซํผ์ ๋ฌ๋ผ๋ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ๊ณ , ๋ฐ์ดํฐ์ ํ๋ฆ์ ๊ด๋ฆฌํ๋ฉฐ, ์ธ๋ถ ๋ณํ๋ก๋ถํฐ ํต์ฌ ๋ก์ง์ ๋ณดํธํ๋ ค๋ ์ค๊ณ ์์น์ ํฌ๊ฒ ๋ค๋ฅด์ง ์์์ต๋๋ค. ์ฌ์ฉํ๋ ๊ธฐ์ ๊ณผ ๊ตฌํ ๋ฐฉ์์ ๋ฌ๋ผ๋ ๋ณต์ก์ฑ์ ๊ด๋ฆฌํ๊ธฐ ์ํ ๊ณ ๋ฏผ์ ๋น์ทํ ๋ฐฉํฅ์ ํฅํ๊ณ ์์์ต๋๋ค.
์ด๋ฒ ๊ฒฝํ์ ํตํด ์๋ก์ด ํ๋ซํผ์ ์ดํดํ ๋๋ ์ฌ์ฉํ๋ ๊ธฐ์ ๋ณด๋ค ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ํ๋ฅด๊ณ , ๊ฐ ๊ณ์ธต์ด ์ด๋ค ์ญํ ์ ๋ด๋นํ๋์ง๋ฅผ ๋จผ์ ์ดํด๋ณด๋ ๊ฒ์ด ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋ ๋ฐ ๋์์ด ๋๋ค๋ ์ ์ ๋๊ผ์ต๋๋ค.