저번 개모임이 끝난지 벌써 2주가 지났다. 사실 어떻게 지나갔나 모르겠다... 정신없이 놀고 먹고... 최근에 운동은 많이 안하고 많이 먹기는 했다. 일 좀 하고, 게임도 하고, 뭐하고 뭐하고… 지내고 있는 것 같다.
요즘 회사는 카카오톡 선물하기 입점부터 뭔가 하나씩 되고 있기는 하다(왜인지는 모름)... 다음주는 벌써 강남 신세계랑 콜라보로 10일동안 팝업이 열린다. 나름 제일 큰 이벤트여서 다들 업무 우선순위가 신세계랑 몰려있다... 근데 인원 부족해서 지원하러 간다. 하하... 물론 연차는 나오지만 가기 싫긴 하다. 그 외 별개로는 무슨 SK OI이런거랑도 뭐 제휴 AI쪽으로 맺었다는데 일단 우리 회사는 데이터분석가는 없긴하다... 이것도 뭐 언젠가는 흘러가겠지.
여튼 개인적으로도 많이 놀기는 했지만 뭔가 한 건 많은 것 같다. 일단 소소하게는 저번 개모임때 이야기했던(마이그레이션 땜에 바쁘다던 핑계로 발표자료는 만들지 못했던…) 외주 개발 사이트도 촘촘 개발하는 중이고, 학교 졸업을 위한 과제 만들기용으로, 사실 그냥 내가 관심이 크게 없는 분야의 글쓰기 싫어서 개발한 보고서 작성기 개발이라던지...
또 최근에는 동네 개발자 친구들과 내가 인턴 시작할때부터 조금씩 구상만 하고 해야지 해야지하면서 쳐박아놨던 개발자 동아리 기획도 생각하고 있다. 솔직히 이제는 기술이 너무 좋아져서 부담감이 덜어진 것도 있다. 그렇다고 아무것도 하지 않고 지켜만 보기는 또 힘들다고 판단이 들어 시작하게 되었다.
뭐 여튼 예상치 못한 변화들과 일들이 나를 더 성장시킨다는 점은 맞는 것 같다. 그리고 진수님이 저번 개모임때 말씀해주신 것처럼 최근에 개발을 하면 할수록 어떤 문제가 생기면 일주일만 지나면 기술이 발전해서 그 문제가 해결된다라는 말에 동감이 가는 그런 시간이었다.
지금부터 아래 내용의 글은 쓰는 시작 시점 오늘(6월 9일) 어제(6월 8일)에 대한 내용이다.
세상이 얼마나 빠른지 내가 느끼는 일들의 속도감이 아래와 같다.
연휴 푹 쉬고보니 일요일이 되었고(어제임… 시간 버그난 것 같다), 평일 내내 쉬엄쉬엄 지내다가, 나름 취업계 학생이라 이번 학기 졸업은 반드시 해야 했기에... "그냥 교양 한 과목 때문에 걸리는 건 졸업시켜주지 ㅜㅜ"는 속마음이었지만, 어쨌든 제대로 계산 안 하고 군대로 런친건 내 잘못이니까.
졸업을 위해 과제를 해야 하는데, 사실 일전에도 강의를 거의 보지 않아서 영상을 다 보고 소감문을 쓰기에는 솔직히 쉽지 않았다... 그렇게 폰을 보며 누워있는데 "그냥 보고서를 자동화할까?"라는 생각이 들었다.
결론은 성공! 로직은 이렇다.

결과는 위와 같이 나왔다. AI와 순수 코딩으로만 구현한 것이었다. 글의 내용은 내가 대충 느낀 점과 나의 배경, 그리고 일전에 교양 문제를 풀 때의 이야기를 큐 데이터와 Whisper AI가 파싱한 텍스트를 기반으로 작성했다.
한 30분 정도의 작업으로 6시간 정도의 시간은 세이브된 것 같다.
이것도 어제 일요일에 동네 개발자 친구들 모여 공부(라고 부르고 노가리까기임)하던 중, 방금 위에서 자칭 보고서 generator?를 만들고 보니 생각이 든 것이다. 나랑 평소 친구들은 삼삼오오 많이 모여 개발을 하곤 했고, 늘 언젠가 내 친구들, 내 지인들과 무언가를 만들어 돈을 벌어보고 싶은 욕구가 컸다. 그것이 내가 상상하는 멋있는 개발자라고 생각했다.
나는 사실 개발자라는 직군이 매력적이게 느껴진 이유도 내 사람들과 즐겁게 자유로운 환경에서 일을 하는 것이 목표였을지도 모른다.

그래서 서두에서 언급했던 동아리를 만들고 싶다는 생각이 들었고, 일전에 개모임에서 언급한 적 있듯, 언젠가 프로덕션을 주위 사람들과 만들어 배포하고 싶다는 생각이 지금의 기술과 그런 것들이 맞아떨어지게 되었고, 어제 같이 공부하던 친구들과의 이야기는 확신으로 가득차게 만들었다.
밥을 먹고 집에 돌아와서 인스타 스토리에 동아리를 같이하자는 글을 올렸고, 얼추 20명이 넘는 지인분들한테 연락이 왔다. 앞으로 나름 재밌는 일들이 시작될 것 같다.
나름 동아리의 이름도 지었다. 사실 지금 이 글에 쓸까 말까 고민하긴 했는데, 이름은 ”콩방”이다. 코딩과 공방을 합친 것 같은 느낌을 주고 싶었고 "코드를 깎는다"라는 단어가 예전에 한번 꽂힌 적이 있어서 내가 하자고 아이디어를 냈다(이건 AI 아이디어 아님). 로고도 만들었는데 이건 나중에 공개하도록 하겠다.
그리고 어떤 것들을 하는 집단인지 궁금하다면 아마 내가 여유가 된다면 7월 전후에 배포될 사이트를 기대해주면 될 것 같다. 사실 레이아웃도 잡았다.
위 모든 내용이 오늘 퇴근하고 저녁먹고 무인카페 가서 전날 같이 하자고 한 개발자 친구 및 LLM과 20분 동안 기획 및 개발(로고 디자인도 함)한 내용이다. 무인카페에서 정말 오후 9시 40분부터 오후 10시까지만 하고 운동갔다가 12시에 집에 왔다... 이제 곧 자고 또 출근해야한다니 ㅠㅠ
개인적인 이야기는 잠시 넣어두고, 이번에는 회사 이야기이다. 최근 다시 어느정도 마이그레이션이 끝나 백오피스 개발에 나름 시간을 할애하고 있다. 뭐 사실 업무가 어렵지는 않고 CRUD API의 확장 정도의 개발이라 큰 즐거움을 얻지는 못하고 있었다.
그러나 드디어 오늘(월요일) 재밌는 부분을 손보게 되었다. 그것은 바로 딥링크다.
일단 딥링크가 뭔지에 대해서 간단하게 설명하자면, 딥링크는 앱이 깔려있을 때 특정 버튼 예를 들면 카카오톡의 바로가기 같은 것들을 누르면 해당 링크로 앱이 연결되게 만드는 그런 장치다. 이게 우리회사는 일단 구글의 무료 날먹 에디션인 Firebase Dynamic Links를 사용하고 있었다.
그러나, 올해 8월 25일 이후로 완전 종료를 한다. 써있는 말로는 단순 지원 종료가 아닌 아예 8월 25일 이후로는 동작을 안하는 것 같다. 사실 뭐 동작을 하든 안하든 크게 중요하지 않고 바꿔야된다고 생각은 하고 있었다...
그러나 얼추 신세계 팝업 갔다오고 개발 좀 하고 예비군도 다녀오면 금방 8월이 될 것 같은 느낌이 쎄게 들어 마침 알림톡도 느려 원래 쓰고 있던 알리고 서비스를 버리고 딥링크랑 알림톡을 동시에 해결할 수 있는 솔루션 업체에 돈을 주고 API를 연동할까 하는 찰나에 오늘 회의에서 언급이 되어버렸다...
뭐 사실 진짜 곧 작업할려고는 했지만, 오늘 개발하게 될 줄은 몰랐는데.
결론적으로 딥링크 찾아보다가 솔루션 해봤자 돈만 더 나가는데 그냥 직접 구현하기로 마음 먹었다.
여러가지 래퍼런스를 보고 AI랑 이야기하다보니 생각보다 로직이 어렵지 않다는걸 느꼈다. 원래 파이어베이스의 딥링크가 잘 작동했다면 구현할 생각도 안했겠지만 오히려 이런 일들이 새로운 경험을 쌓게 해주는 것 같다.
그래서 딥링크를 찾아보며 딥링크, 유니버셜링크, 앱링크 등을 알게 되었고, 각각의 차이점을 파악해보았다.
모바일 애플리케이션을 자동으로 열어 앱 내의 특정 화면으로 보낼 수 있는 링크다.
com.company.service://product/123)TypeScript/ 기존 우리가 사용하던 딥링크 방식 const deepLinkUrl = "com.company.service://product/123"; // 문제: 앱이 없으면 에러 페이지만 표시 -> 파이어베이스는 앱이 없을 때 동작 설정 가능
일반적인 웹 URL과 바인딩되어 훨씬 자연스러운 사용자 경험을 제공한다.
TypeScript// 유니버셜 링크 방식 const universalLinkUrl = "https://service.co.kr/api/deeplink/universal/abc123"; // 앱이 있으면 → 앱 실행 // 앱이 없으면 → 웹 페이지 또는 앱스토어로 이동
Android에서 제공하는 유니버셜 링크와 유사한 기능이다.
이 중 구현을 선택한 것은 유니버셜 링크였다.
이유는 단순히 우리는 지금 전문적인 iOS/Android 앱개발자는 없기 때문에 최대한 네이티브 코드를 수정하지 않아도 되는 유니버셜 링크로 선택을 했고, 그 와중에 iOS가 항상 더 민감하고 짜증나는 경우가 많았기에 유니버셜 링크로 백엔드에서 구현을 시작하기로 했다.
사실 백엔드를 구현하고 프론트로 연결하고 프론트에 prefix link.domain.com 이런식으로 만들어서 연결하는 것이 보안상을 훨씬 좋은 구조인 것 같긴하지만 이 과정이 너무 귀찮았다 ㅎㅎ, 배포 전에 도커에 말려있는 nginx 열어서 일전 부분 config로 url 허용해주는게 귀찮았다... 그래서 일단 테스트 서버 도메인에서는 걍 백엔드로만 단일 구현을 시작하였다.
그래서 구현은 아래와 같다.
유니버셜 링크의 핵심은 apple-app-site-association 파일이다. 이 파일로 "이 웹사이트는 이 앱과 연결되어 있어요!"라고 애플에게 알려준다.
TypeScript// Apple App Site Association 미들웨어 @Injectable() export class AppleAppSiteAssociationMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (req.path === '/.well-known/apple-app-site-association') { const appSiteAssociation = { applinks: { apps: [], details: [ { appID: 'TeamID.com.company.service', // Team ID + Bundle ID paths: ['/api/deeplink/universal/*', '*'], }, ], }, }; res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'public, max-age=3600'); res.json(appSiteAssociation); return; } next(); } }
하나의 URL이지만 플랫폼에 따라 다른 경험을 제공하도록 구현했다.
TypeScript/** * Universal Link 처리 - 하나의 URL, 세 가지 경험 */ @Get('universal/:shortCode') async handleUniversalLink( @Param('shortCode') shortCode: string, @Req() req: Request, @Res() res: Response, ): Promise<void> { const userAgent = req.headers['user-agent'] || ''; const isIOS = /iPhone|iPad|iPod/i.test(userAgent); const isAndroid = /Android/i.test(userAgent); try { const deepLink = await this.deeplinkService.getDeepLinkByCode(shortCode, req); if (isIOS) { // iOS: 앱이 있으면 자동 실행, 없으면 HTML 페이지 표시 const html = this.generateIOSFallbackHTML(deepLink); res.setHeader('Content-Type', 'text/html'); res.send(html); } else if (isAndroid) { // Android: Intent URL로 앱 실행 시도 const intentUrl = `intent://...#Intent;scheme=com.company.service;...`; res.redirect(intentUrl); } else { // Desktop: 웹으로 리다이렉트 res.redirect(deepLink.fallbackWebUrl || 'https://service.co.kr'); } } catch (error) { res.redirect('https://service.co.kr'); } }
앱이 설치되어 있지 않은 경우를 위한 정교한 fallback 페이지를 만들었다. 백엔드 프론트엔드 코드 연결하기 귀찮아서 테스트 환경이니 걍 백엔드에서 호출하면 프론트 페이지를 넘겨주었다.
TypeScriptprivate getIOSRedirectHTML(originalUrl: string, fallbackWebUrl: string): string { const appStoreUrl = 'https://apps.apple.com/kr/app/id12341234'; const urlScheme = 'com.company.service'; return ` <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>앱으로 이동 중...</title> <!-- 생략된 스타일 --> </head> <body> <div class="logo">🍽️</div> <h1>앱으로 이동 중...</h1> <div class="loading">앱이 설치되어 있다면 자동으로 실행됩니다</div> <div class="buttons"> <a href="#" onclick="tryOpenAppManual()" class="btn btn-primary">앱 실행</a> <a href="${appStoreUrl}" class="btn">앱스토어에서 설치</a> <a href="${fallbackWebUrl}" class="btn">웹에서 보기</a> </div> <script> // 자동 앱 실행 시도 로직 // 페이지 가시성 변경 감지 (앱이 실행되면 페이지가 숨겨짐) // 디버깅 정보 표시 </script> </body> </html> `; }
기존의 딥링크 클릭 추적 기능은 그대로 유지하면서 유니버셜 링크에도 적용했다.
TypeScript// 기존 딥링크는 여전히 작동하되, 내부적으로는 동일한 로직 사용 @Get(':shortCode') async redirectDeepLink( @Param('shortCode') shortCode: string, @Req() req: Request, @Res() res: Response, ): Promise<void> { const result = await this.deeplinkService.getDeeplinkRedirectResult(hortCode, req.headers['user-agent'] || '', req.ip || ''); if (result.type === 'html') { res.setHeader('Content-Type', 'text/html'); res.send(result.html); } else { res.redirect(302, result.url); } }
이제 iOS 앱 쪽에서도 유니버셜 링크를 받아 처리할 수 있도록 코드를 추가했다.
먼저 SceneDelegate에서 URL을 받아 처리하는 부분
TypeScript//카카오 로그인, 네이버 로그인, 딥링크 처리 func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { if let url = URLContexts.first?.url { print("SceneDelegate received URL: \(url)") if AuthApi.isKakaoTalkLoginUrl(url) { //카카오 로그인 _ = AuthController.handleOpenUrl(url: url) } else if donaverlogin { // 네이버 로그인 NaverThirdPartyLoginConnection .getSharedInstance()? .receiveAccessToken(URLContexts.first?.url) donaverlogin = false } else if url.scheme == "com.company.service" { // 딥링크 처리 print("Deep link opened in SceneDelegate: \(url)") // AppDelegate의 handleUniversalDeepLink 함수 호출 if let appDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.handleUniversalDeepLink(url: url) } } } else { // 이상 print("로그인 아닌 경우 (오류)") } }
그리고 AppDelegate에서 실제 딥링크를 처리하는 로직
TypeScript// MARK: DeepLink Handling func handleUniversalDeepLink(url: URL) { print("Handling universal deeplink: \(url)") guard url.scheme == "com.company.service" else { print("Invalid URL scheme for deeplink: \(url.scheme ?? "nil")") return } let host = url.host let path = url.path print("Deeplink host: \(host ?? "nil"), path: \(path)") // Convert deeplink to web URL navigateToWebURL(host: host, path: path) } private func navigateToWebURL(host: String?, path: String) { print("Converting deeplink to web URL - host: \(host ?? "nil"), path: \(path)") DispatchQueue.main.async { let baseURL = "https://service.co.kr" var webURL: String // Build the appropriate web URL based on deeplink components if let host = host, !host.isEmpty { // If host exists, use it as the main path if path.isEmpty || path == "/" { webURL = "\(baseURL)/\(host)" } else { // Combine host and path webURL = "\(baseURL)/\(host)\(path)" } } else if !path.isEmpty && path != "/" { // If only path exists, use it directly webURL = "\(baseURL)\(path)" } else { // Default to main page if no meaningful path webURL = baseURL } print("Generated web URL: \(webURL)") // Use the existing PUSH_URL mechanism that MainViewController already handles UserDefaults.standard.set(webURL, forKey: "PUSH_URL") UserDefaults.standard.synchronize() // Post notification using the existing OpenWebViewWithURL pattern let notificationInfo: [String: String] = ["url": webURL] NotificationCenter.default.post(name: Notification.Name("OpenWebViewWithURL"), object: nil, userInfo: notificationInfo) print("Set PUSH_URL to: \(webURL) and posted OpenWebViewWithURL notification") } }
여기서 핵심은 기존의 푸시 알림 처리 메커니즘을 그대로 활용했다는 점이다.
딥링크로 들어온 URL을 웹 URL로 변환한 후, 기존에 푸시 알림에서 사용하던 PUSH_URL UserDefaults와 OpenWebViewWithURL 노티피케이션을 그대로 사용해서 MainViewController에서 처리하도록 했다.
이렇게 하면 별도의 딥링크 전용 네비게이션 로직을 만들 필요 없이, 기존의 잘 작동하는 시스템을 재활용할 수 있어서 효율적이었다.
여튼 점심 이후 퇴근 전까지 구현 과정에서 몇 가지 큰 삽질이 있었다.
첫 번째 삽질: Team ID 혼동
처음에는 Bundle ID만 넣어도 될 줄 알았는데, TeamID.com.company.service 형태로 Team ID와 Bundle ID를 모두 포함해야 한다는 걸 뒤늦게 깨달았다.
두 번째 삽질: 캐시의 함정 Apple App Site Association 파일을 수정해도 iOS에서 즉시 반영되지 않는 경우가 있었다. 애플 서버에서 이 파일을 캐싱하기 때문에 몇 시간씩 기다려야 했다.
세 번째 삽질: 디버깅의 어려움 유니버셜 링크가 왜 작동하지 않는지 디버깅하기가 어려워 HTML fallback 페이지에 실시간 디버그 정보를 추가했다. 그래서 걍 스위프트 웹 환경에서 로그를 봤다.
정말 이제는 이런 구현 자체도 확실히 예전보다 빨라졌다고 느낀다. 불과 이 과정이 점심 먹고 퇴근 전까지 구현이 된다는 것 자체가 정말 신기함을 느낀다. 선대 개발자들이 쌓아올린 지식을 먹은 AI는 정말 유익과 동시에 해롭다.
솔직히 말하면, 가끔은 내가 하고 싶은 일들의 속도를 내 자신이 못 따라갈 것 같다는 생각이 든다.
오늘 하루만 봐도 그렇다. 아침에 딥링크 구현하겠다고 마음먹고, 점심 먹고 와서 몇 시간 만에 유니버셜 링크까지 완성했다. 분명 예전 같았으면 며칠은 걸렸을 일인데... 이게 AI 덕분인지, 아니면 그동안 쌓인 경험 때문인지 모르겠다. 어쨌든 무섭도록 빠르게 돌아가는 세상 속에서 나도 모르게 휩쓸려 가고 있는 기분이다.
한편으로는 이런 기술의 발전이 나를 앞으로 끌어주고 있다는 느낌도 든다. 혼자서는 절대 할 수 없었을 일들이 가능해지고, 상상만 했던 것들이 현실이 되고... 하지만 동시에 이걸 언제까지 따라갈 수 있을까? 하는 불안감도 크다.
특히 요즘 같은 때는 더 그렇다. 회사 일도 해야 하고, 개인 프로젝트도 하고 싶고, 동아리도 만들어야 하고, 졸업도 해야 하고... 하나하나는 별로 어렵지 않은데, 전체적으로 보면 숨이 찰 때가 많다. "내가 이걸 다 할 수 있을까?" 하는 의문이 들 때도 있고.
그래도 뭐 우리는 서로 의지하며 살아가는 거 아닌가라는 생각 더 크다. 내가 오늘 딥링크로 삽질한 경험이 누군가에게는 시간을 아껴주는 도움이 될 수도 있고, 반대로 누군가의 경험담이 내게 큰 힌트가 될 수도 있겠지.
완벽해 보이려 하지 말고, 서로 솔직하게 어려움도 나누고 도움도 요청하면서 함께 성장해 나가면 좋겠다. 이 빠른 세상 속에서 혼자서는 버티기 힘들지만, 함께라면 충분히 따라갈 수 있을 것 같다.
이제 다음 개모임에서는 신세계 팝업 운영 후기와 카카오 선물하기 입점 과정에서의 이야기들을 풀어놓을 수 있을 것 같다. 그리고 언제나 그렇듯, 혹시 딥링크나 유니버셜 링크로 고생하고 있는 동료 개발자가 있다면 언제든 연락 주시길... 삽질의 경험을 나누는 것도 개발자 동료애 아니겠는가.