Skip to content

Commit 2b33389

Browse files
committed
add post
1 parent 7173f1f commit 2b33389

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
---
2+
layout: post
3+
title: "[iOS] WKWebView을 이용한 iOS 앱과 웹페이지 간의 통신 (3) - Plugin을 이용하여 기능 확장하기"
4+
tags: [iOS, WKWebView, Javascript, if, switch, statement, Plugin]
5+
---
6+
{% include JB/setup %}
7+
8+
이전 글에서 웹페이지에서 전달한 Action을 처리하는 조건문의 구현이 계속 늘어나, 모든 기능을 포함하도록 된다는 것을 알 수 있었습니다.
9+
10+
이번 글에서는 `WKWebView`와 다른 도메인과의 강한 결합 관계를 피하기 위해 Plugin을 이용하여 기능을 확장하는 방법을 알아보려고 합니다.
11+
12+
## Plugin
13+
14+
Plugin이란, 특정 기능을 수행하는 코드를 따로 분리하는 것을 의미합니다. Plugin을 이용하면 기능을 확장하거나, 기능을 수정할 때 기존 코드를 수정하지 않고도 기능을 추가할 수 있습니다.
15+
16+
웹페이지에서 전달하는 Action을 처리하는 조건문의 코드를 Plugin으로 분리하여, 기존 코드를 수정하지 않도고 새로운 Plugin을 추가함으로써 기능을 추가할 수 있습니다.
17+
18+
### Plugin 구현
19+
20+
Plugin을 구현하기 전에, 웹페이지에서 iOS 앱으로 전달하는 전달하는 JSON 구조는 다음과 같습니다.
21+
22+
```json
23+
{
24+
"action": "action",
25+
"uuid": "uuid",
26+
"body": "body"
27+
}
28+
```
29+
30+
body는 String, Int, Bool, Array, Dictionary 등의 타입을 가지는 값으로 구성됩니다. Action에 따라 body의 구조가 달라지므로 Plugin에서 body를 파싱하는 방법을 구현해야 합니다.
31+
32+
Plugin은 Action을 Key로 사용하고, message를 넘겨받을 수 있도록 하는 `callAsAction` 메소드를 가지는 `JSInterfacePluggable` 프로토콜을 구현합니다.
33+
34+
```swift
35+
// FileName : JSInterfacePluggable.swift
36+
37+
import WebKit
38+
39+
protocol JSInterfacePluggable {
40+
var action: String { get }
41+
func callAsAction(_ message: [String: Any], with: WKWebView)
42+
}
43+
```
44+
45+
다음으로, Plugin을 관리하는 Supervisor를 만들고, 웹뷰로부터 특정 Action을 수행 요청을 받으면 Plugin의 callAsAction을 호출하도록 하는 기능을 구현합니다.
46+
47+
```swift
48+
// FileName : JSInterfaceSupervisor.swift
49+
50+
import Foundation
51+
import WebKit
52+
53+
/// Supervisor class responsible for loading and managing JS plugins.
54+
class JSInterfaceSupervisor {
55+
var loadedPlugins = [String: JSInterfacePluggable]()
56+
57+
init() {}
58+
}
59+
60+
extension JSInterfaceSupervisor {
61+
/// Loads a single plugin into the supervisor.
62+
func loadPlugin(_ plugin: JSInterfacePluggable) {
63+
let action = plugin.action
64+
65+
guard loadedPlugins[action] == nil else {
66+
assertionFailure("\(action) action already exists. Please check the plugin.")
67+
return
68+
}
69+
70+
loadedPlugins[action] = plugin
71+
}
72+
73+
/// Loads multiple plugins into the supervisor.
74+
func loadPlugin(contentsOf newElements: [JSInterfacePluggable]) {
75+
newElements.forEach { loadPlugin($0) }
76+
}
77+
}
78+
79+
extension JSInterfaceSupervisor {
80+
/// Resolves an action and calls the corresponding plugin with a message and web view.
81+
func resolve(_ action: String, message: [String: Any], with webView: WKWebView) {
82+
guard
83+
let plugin = loadedPlugins[action],
84+
plugin.action == action
85+
else {
86+
assertionFailure("Failed to resolve \(action): Action is not loaded. Please ensure the plugin is correctly loaded.")
87+
return
88+
}
89+
90+
plugin.callAsAction(message, with: webView)
91+
}
92+
}
93+
```
94+
95+
`JSInterfaceSupervisor``JSInterfacePluggable` 프로토콜을 준수하는 Plugin을 관리하는 역할을 수행합니다. `JSInterfacePluggable` 프로토콜을 준수하는 Plugin을 `loadPlugin` 메소드를 이용하여 로드하고, `WKWebView`로부터 Action을 수행 요청을 받으면, `resolve` 메소드를 호출하여 Plugin을 호출합니다.
96+
97+
이제 웹페이지에서 전달받은 Action을 처리하는 조건문의 코드를 Plugin으로 분리합니다.
98+
99+
```swift
100+
// Before
101+
102+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
103+
// 메시지의 이름과 body 추출
104+
guard
105+
message.name == "actionHandler",
106+
let messageBody = message.body as? [String: Any],
107+
let action = messageBody["action"] as? String
108+
else { return }
109+
110+
// Action에 따라 처리하는 switch 문
111+
switch action {
112+
case "loading": loading(body: messageBody)
113+
case "openCard": openCard(body: messageBody)
114+
case "payment": payment(body: messageBody)
115+
case "log": log(body: messageBody)
116+
default: break
117+
}
118+
...
119+
}
120+
121+
// After
122+
123+
private let supervisor = JSInterfaceSupervisor()
124+
125+
func set(plugins: [JSInterfacePluggable]) {
126+
supervisor.loadPlugin(contentsOf: plugins)
127+
}
128+
129+
...
130+
131+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
132+
// 메시지의 이름과 body 추출
133+
guard
134+
message.name == "actionHandler",
135+
let messageBody = message.body as? [String: Any],
136+
let action = messageBody["action"] as? String
137+
else { return }
138+
139+
// Supervisor에게 Action을 수행 요청
140+
supervisor.resolve(action, message: messageBody, with: webView)
141+
}
142+
```
143+
144+
다음으로, 각 Action에 해당하는 Plugin을 만들어봅시다.
145+
146+
```swift
147+
// MARK: - LoadingJSPlugin
148+
class LoadingJSPlugin: JSInterfacePluggable {
149+
struct Info {
150+
let uuid: String
151+
let isShow: Bool
152+
}
153+
154+
let action = "loading"
155+
156+
func callAsAction(_ message: [String: Any], with webView: WKWebView) {
157+
guard
158+
let result = Parser(message)
159+
else { return }
160+
161+
closure?(result.info, webView)
162+
}
163+
164+
func set(_ closure: @escaping (Info, WKWebView) -> Void) {
165+
self.closure = closure
166+
}
167+
168+
private var closure: ((Info, WKWebView) -> Void)?
169+
}
170+
171+
private extension LoadingJSPlugin {
172+
struct Parser {
173+
let info: Info
174+
175+
init?(_ dictonary: [String: Any]) {
176+
guard
177+
let uuid = dictonary["uuid"] as? String,
178+
let body = dictonary["body"] as? [String: Any],
179+
let isShow = body["isShow"] as? Bool
180+
else { return nil }
181+
182+
info = .init(uuid: uuid, isShow: isShow)
183+
}
184+
}
185+
}
186+
187+
// MARK: - PaymentJSPlugin
188+
class PaymentJSPlugin: JSInterfacePluggable {
189+
struct Info {
190+
let uuid: String
191+
let paymentAmount: Int
192+
let paymentTransactionId: String
193+
let paymentId: String
194+
let paymentGoodsName: String
195+
}
196+
197+
let action = "payment"
198+
199+
func callAsAction(_ message: [String: Any], with webView: WKWebView) {
200+
guard
201+
let result = Parser(message)
202+
else { return }
203+
204+
closure?(result.info, webView)
205+
}
206+
207+
func set(_ closure: @escaping (Info, WKWebView) -> Void) {
208+
self.closure = closure
209+
}
210+
211+
private var closure: ((Info, WKWebView) -> Void)?
212+
}
213+
214+
private extension PaymentJSPlugin {
215+
struct Parser {
216+
let info: Info
217+
218+
init?(_ dictonary: [String: Any]) {
219+
guard
220+
let uuid = dictonary["uuid"] as? String,
221+
let body = dictonary["body"] as? [String: Any],
222+
let paymentAmount = body["paymentAmount"] as? Int,
223+
let paymentTransactionId = body["paymentTransactionId"] as? String,
224+
let paymentId = body["paymentId"] as? String,
225+
let paymentGoodsName = body["paymentGoodsName"] as? String
226+
else { return nil }
227+
228+
info = .init(
229+
uuid: uuid,
230+
paymentAmount: paymentAmount,
231+
paymentTransactionId: paymentTransactionId,
232+
paymentId: paymentId,
233+
paymentGoodsName: paymentGoodsName
234+
)
235+
}
236+
}
237+
}
238+
```
239+
240+
위와 같이 `JSInterfacePluggable` 프로토콜을 준수하는 Plugin을 만들고, Plugin를 생성하고, Closure를 주입한 뒤, `JSInterfaceSupervisor`에 Plugin을 등록하면 됩니다.
241+
242+
```swift
243+
let loadingPlugin = LoadingJSPlugin()
244+
let paymentPlugin = PaymentJSPlugin()
245+
loadingPlugin.set { info, webView in
246+
// Loading Action일 때 수행할 코드
247+
}
248+
paymentPlugin.set { info, webView in
249+
// Payment Action일 때 수행할 코드
250+
}
251+
252+
webViewManager.set(plugins: [loadingPlugin, paymentPlugin])
253+
```
254+
255+
이제 웹페이지에서 Action을 수행하는 코드를 Plugin으로 분리했습니다. 추가되는 Action에 맞춰 Plugin을 만들고, 필요한 Plugin을 등록하는 방식으로 진행하면 됩니다.
256+
257+
이와 같은 방식은 웹페이지에서 전달하는 Action 뿐만 아니라, AppScheme, 채팅 등의 다른 방식에서도 Plugin 방식을 적용하여 코드를 더욱 쉽게 유지하고 관리할 수 있습니다.
258+
259+
## 정리
260+
261+
이번 포스팅에서는 웹페이지에서 Action을 수행하는 코드를 Plugin으로 분리하는 방법에 대해서 알아보았습니다.
262+
263+
웹페이지에서 전달받은 Action을 수행하는 코드를 Plugin으로 분리하는 방식은 코드를 더욱 쉽게 유지하고 관리할 수 있게 해주며, Action을 처리하는 코드를 더욱 간결하게 작성할 수 있습니다.
264+
265+
[예제 코드](https://github.com/minsOne/Experiment-Repo/tree/master/20240511)
266+
267+
## 참고자료
268+
269+
* Post
270+
* [분리 인터페이스 패턴, 플러그인 패턴](https://harrislee.tistory.com/62)
271+
* [Using the plugin pattern in a modularized codebase](https://proandroiddev.com/using-the-plugin-pattern-in-a-modularized-codebase-af8d4905404f)
272+
* [Introduction to Plugin Architecture in C#](https://www.youtube.com/watch?v=g4idDjBICO8)
273+
* [Plug-in Architecture](https://medium.com/omarelgabrys-blog/plug-in-architecture-dec207291800)
274+
* [Extending modules using a plugin architecture](https://medium.com/@tyronemichael/extending-your-modules-using-a-plugin-architecture-c1972735d728)
275+
* [Plugin-Based Architecture and Scaling iOS Development at Capital One](https://medium.com/capital-one-tech/plugin-based-architecture-and-scaling-ios-development-at-capital-one-fb67561c7df6)
276+
* [Swift By Sundell - Making Swift code extensible through plugins](https://www.swiftbysundell.com/articles/making-swift-code-extensible-through-plugins/)
277+
* GitHub
278+
* [ionic-team/capacitor](https://github.com/ionic-team/capacitor/blob/main/ios/Capacitor/Capacitor/CAPBridgeProtocol.swift)
279+
* [capacitor-community/generic-oauth2](https://github.com/capacitor-community/generic-oauth2/blob/276f01d4883748a776e86d80ad9b0b547309561f/ios/Plugin/GenericOAuth2Plugin.swift#L82)
280+
* [TYRONEMICHAEL/plugin-architecture-swift](https://github.com/TYRONEMICHAEL/plugin-architecture-swift)
281+
* [Electrode-iOS/ELMaestro](https://github.com/Electrode-iOS/ELMaestro/blob/master/ELMaestro/Pluggable.swift)
282+

0 commit comments

Comments
 (0)