Thinking
웹뷰환경에서 앱스킴과 인터페이스를 효과적으로 관리하고, 사용하는 방법에 대해서 알아봅시다.
우선 여러가지 개념에 대해서 미리 설명을 드리자면 아래와 같습니다.
딥링킹이란?
액션이란?
인터페이스란?
딥링크를 이동하는 type과 (replace, push) 앱 스킴내 이동위치 등을 enum으로 정의해 type파일에 넣어놓습니다.
MoveType
/**
* move 타입 enum
*/
export enum MoveType {
REPLACE = "replace",
PUSH = "push",
}
Location
/**
* 앱 scheme내 이동 위치
*/
export enum Location {
HOME = 0,
SUB_HOME,
}
Action
/**
* 앱 action 리스트
*/
export enum Action {
CLOSE = "closeWebview",
}
export enum LocationType {
ABSOLUTE = 0,
RELATIVE,
}
LocationType
/**
* Location 타입
*/
export enum LocationType {
ABSOLUTE = 0,
RELATIVE,
}
config
const common = {
HOME: "home",
};
const config = {
AOS: {
...common,
SUB_HOME: "aos/subHome",
},
IOS: {
...common,
SUB_HOME: "ios/subHome",
},
};
/**
* move 타입 enum
*/
export enum MoveType {
REPLACE = "replace",
PUSH = "push",
}
/**
* 앱 scheme내 이동 위치
*/
export enum Location {
HOME = 0,
SUB_HOME,
}
/**
* 앱 action 리스트
*/
export enum Action {
CLOSE = "closeWebview",
}
/**
* Location 타입
*/
export enum LocationType {
ABSOLUTE = 0,
RELATIVE,
}
const common = {
HOME: "home",
};
// 아래와 같이 구성한 이유는 aos, ios 공통의 앱스킴이 있을수 있는반면, 같은 링크인데도 다른 앱스킴이 있을 수 있으므로 아래와 같이 구성.
const config = {
AOS: {
...common,
SUB_HOME: "aos/subHome",
},
IOS: {
...common,
SUB_HOME: "ios/subHome",
},
};
export default config;
import { toQueryString } from "utils";
import config, { MoveType, LocationType, Action } from "./types";
export interface IAppProtocolState {
move(
location: number,
locationType: LocationType,
moveType: MoveType,
params: object
): void;
action(actionName: Action, moveType: MoveType, params: object): void;
callNativeHandler(methodName: string, params: any): void;
}
abstract class AppProtocol implements IAppProtocolState {
protected actionEntry: string | undefined;
protected entry: string | undefined;
constructor() {
this.actionEntry = process.env.REACT_APP_DEEP_LINK_ACTION_PROTOCOL;
this.entry = process.env.REACT_APP_DEEP_LINK_PROTOCOL;
}
/**
* Action function
* @param action 실행할 action
* @param moveType move타입
* @param params 같이 보낼 param
*/
action(actionName: Action, moveType: MoveType, params = {}) {
let url = `${this.actionEntry}${actionName}`;
url += toQueryString(params);
if (moveType === MoveType.PUSH) window.location.href = url;
else window.location.replace(url);
}
// move나 callNativeHandler는 추상 메소드로 만들어, 상속받은 객체에 구현을 위임하였습니다.
abstract move(
location: number,
locationType: LocationType,
moveType: MoveType,
params: object
): void;
abstract callNativeHandler(methodName: string, params?: any): void;
}
export class IOSProtocol extends AppProtocol {
constructor() {
super();
}
/**
* Move function
* @param location 위치
* @param moveType move 타입
* @param params 같이 보낼 param
*/
move(
location: number,
locationType: LocationType,
moveType: MoveType,
params = {}
) {
const locations = config["IOS"];
const locationPrefix = locationType === LocationType.RELATIVE ? "./" : "";
let url = `${this.entry}${locationPrefix}${
Object.values(locations)[location]
}`;
url += toQueryString(params, true);
if (moveType === MoveType.PUSH) window.location.href = url;
else window.location.replace(url);
}
callNativeHandler(methodName: string, params: any) {
const hasNativeHandler = Boolean(
window.webkit &&
window.webkit.messageHandlers &&
window.webkit.messageHandlers[methodName]
);
if (!hasNativeHandler) {
return;
}
try {
const stringifiedParams =
typeof params === "string" ? params : JSON.stringify(params);
window.webkit.messageHandlers[methodName].postMessage(stringifiedParams);
} catch (error) {
console.error(`failed to call ios native handler ${methodName}`, error);
}
}
}
export class AOSProtocol extends AppProtocol {
constructor() {
super();
}
/**
* Move function
* @param location 위치
* @param moveType move 타입
* @param params 같이 보낼 param
*/
move(
location: number,
locationType: LocationType,
moveType: MoveType,
params = {}
) {
const locations = config["AOS"];
const locationPrefix = locationType === LocationType.RELATIVE ? "./" : "";
let url = `${this.entry}${locationPrefix}${
Object.values(locations)[location]
}`;
url += toQueryString(params, true);
if (moveType === MoveType.PUSH) window.location.href = url;
else window.location.replace(url);
}
callNativeHandler(methodName: "string", params: any) {
const hasNativeHandler =
window."정의한 인터페이스명" &&
typeof window.정의한 인터페이스명[methodName] === "function";
if (!hasNativeHandler) {
return;
}
try {
const stringifiedParams =
typeof params === "string" ? params : JSON.stringify(params);
stringifiedParams
? window."정의한 인터페이스명"[methodName](stringifiedParams)
: window."정의한 인터페이스명"[methodName]();
} catch (error) {
console.error(
`failed to call android native handler ${methodName}`,
error
);
}
}
}
export class AppManager {
private state: IAppProtocolState;
constructor(state: IAppProtocolState) {
this.state = state;
}
get State(): IAppProtocolState {
return this.state;
}
set State(state: IAppProtocolState) {
this.state = state;
}
action(action: Action, moveType: MoveType, params = {}) {
this.state.action(action, moveType, params);
}
move(
location: number,
locationType: LocationType,
moveType: MoveType,
params = {}
) {
this.state.move(location, locationType, moveType, params);
}
callNativeHandler(methodName: string, params: any) {
this.state.callNativeHandler(methodName, params);
}
}
import {
AppManager,
AOSProtocol,
IOSProtocol,
} from "hooks/lib/AppProtocolManager";
import {
MoveType,
Action,
LocationType,
Location,
} from "hooks/lib/AppProtocolManager/types";
interface IParam {
deviceType: DeviceType;
}
function useAppManager(param: IParam) {
const isAndroid = param.deviceType === "android";
// 아래와 같이 aos ios 여부에 따라 AOSProtocol 혹은 IOSProtocol 상태 객체를 넣어주게 됩니다.
const context = new AppManager(
isAndroid ? new AOSProtocol() : new IOSProtocol()
);
const appAction = (
actionName: Action,
moveType = MoveType.REPLACE,
params: object
) => {
context.action(actionName, moveType, params);
};
const appMove = (
location: number,
locationType: LocationType,
moveType: MoveType,
params: object
) => {
context.move(location, locationType, moveType, params);
};
const appInterfaceHandler = (methodName: string, params?: any) => {
context.callNativeHandler(methodName, params);
};
return { appAction, appMove, appInterfaceHandler };
}
export { MoveType, Action, LocationType, Location };
export default useAppManager;
const { appAction, appMove, appInterfaceHandler } = useAppManager({
'android'
// user agent에서 ios or aos 판별해서 넘겨중
});
appMove(Location.HOME, LocationType.ABSOLUTE, MoveType.PUSH, {
param: 1,
});
appAction(Action.CLOSE, MoveType.REPLACE, {});
appInterfaceHandler("호출할 인터페이스명", { param: "paramtest" });