리스트나 그리드 형태의 UI에서 많은 양의 이미지를 불러와야 할 때 메인 스레드에서 모든 과정을 처리하면 화면이 버벅거리거나 멈추는 현상이 발생할 수 있어요. 특히 이미지 데이터를 네트워크로 가져오고 이를 디코딩하여 메모리에 올리는 과정은 상당한 CPU 자원을 소모하기 때문에 반드시 백그라운드 환경에서 수행해야 하거든요. 이때 실무에서 가장 기본적으로 활용하는 도구가 바로 Grand Central Dispatch(GCD)예요. GCD를 사용하면 복잡한 멀티스레딩 로직을 직접 구현하지 않고도 시스템이 제공하는 큐에 작업을 할당하여 효율적으로 처리할 수 있어요.

func fetchImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
    // 백그라운드 큐에서 데이터 로딩 및 이미지 변환 수행
    DispatchQueue.global(qos: .userInitiated).async {
        if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
            // UI 업데이트는 반드시 메인 스레드에서 수행
            DispatchQueue.main.async {
                completion(image)
            }
        } else {
            DispatchQueue.main.async {
                completion(nil)
            }
        }
    }
}

위 코드에서 `DispatchQueue.global`을 사용하는 이유는 메인 스레드와 별개로 작업을 분리하기 위해서예요. 네트워크 통신이나 데이터 변환 같은 무거운 작업이 백그라운드에서 실행되는 동안 사용자는 UI를 멈춤 없이 조작할 수 있게 되거든요. 하지만 가장 중요한 포인트는 마지막에 이미지를 `UIImageView`나 다른 UI 컴포넌트에 할당하는 과정이에요. iOS 환경에서는 모든 UI 업데이트가 메인 스레드에서만 이루어져야 하기 때문에, 백그라운드에서 작업을 마친 후 반드시 `DispatchQueue.main.async`를 통해 다시 메인 스레드로 돌아와야 해요. 이 흐름을 놓치면 화면이 갱신되지 않거나 앱이 비정상적으로 종료될 수 있거든요.

단순히 비동기 처리를 적용하는 것 외에도 실무에서는 메모리 관리 측면을 함께 고려해야 해요. 동일한 URL에서 반복적으로 이미지를 불러올 때 매번 네트워크 요청을 보내지 않도록 `NSCache`를 활용하여 한 번 로드한 이미지를 캐싱해두는 방식이 일반적이에요. 최근에는 Swift Concurrency가 도입되면서 `async/await` 구문을 사용할 수도 있지만, 기존 코드와의 호환성이나 세밀한 큐 제어가 필요한 상황에서는 여전히 GCD가 강력한 도구로 쓰이고 있어요. 적절한 큐 선택과 메인 스레드 복귀 시점을 정확히 파악하는 것이 매끄러운 사용자 경험을 만드는 핵심이에요.

iOS 앱에서 서버와 데이터를 주고받을 때 가장 기본적으로 쓰는 게 URLSession이거든요. 최근에는 async/await가 주류지만, 여전히 기존 코드베이스나 특정 프레임워크에서는 dataTask를 직접 다루는 경우가 많아요. 이때 응답 데이터 형식을 올바르게 처리하고 에러를 잡지 않으면 앱이 크래시나거나 로딩 상태가 멈춰 버리더라고요. 기본 설정의 타임아웃은 사실 꽤 길어요. timeoutIntervalForRequest는 60초, timeoutIntervalForResource는 7일로 되어 있어서 느린 네트워크에서 바로 실패하지 않아요. 다만 특정 서비스에서는 응답 대기 시간을 명확히 하거나 커스텀 헤더를 추가해야 할 경우가 많거든요. 그래서 URLSessionConfiguration 객체를 생성해서 timeoutIntervalForRequest와 httpAdditionalHeaders를 직접 설정하는 게 좋아요.

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 604800
let session = URLSession(configuration: config)

var request = URLRequest(url: URL(string: "https://api.example.com/data")!)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")

session.dataTask(with: request) { data, response, error in
    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { return }
    guard let data = data, let json = try? JSONDecoder().decode(ResponseModel.self, from: data) else { return }
    DispatchQueue.main.async {
        // UI 업데이트 로직
    }
}.resume()

실제 프로젝트에서는 네트워크 상태에 따른 실패 케이스도 고려해야 해요. 에러가 발생했을 때 사용자에게 안내 메시지를 띄우는 로직을 추가하면 훨씬 안정적이에요. 특히 HTTP 상태 코드가 200이 아닌 경우를 명시적으로 체크하는 게 중요한데, dataTask는 네트워크 오류가 없으면 일단 completionHandler를 호출하기 때문이죠. 응답 데이터가 비어 있거나 포맷이 깨진 경우에도 안전하게 처리하려면 guard let 구문을 활용하는 게 좋아요. JSON을 파싱할 때는 DecodingError를 구체적으로 캐치해서 디버깅 용이하게 만들면 좋더라고요. UI 업데이트는 반드시 메인 스레드에서 이루어져야 하므로 DispatchQueue.main.async 안에서 화면을 갱신해야 해요. delegate 방식보다 closure 기반 API가 메모리 관리 측면에서 더 깔끔한 편이니 상황에 맞춰 선택하면 되거든요.

UIStackView를 활용하면 정적 레이아웃은 물론 데이터에 따라 뷰가 늘어날 때도 자동으로 배치를 관리할 수 있죠. 다만 addArrangedSubview 메서드로 동적 생성을 할 때 제약 조건 충돌이 자주 발생하거든요. UIView의 addSubview를 직접 호출하면 프레임이나 NSLayoutConstraint를 일일이 계산해야 하지만, UIStackView는 내부에서 오토레이아웃을 대신 처리해 주더라고요. 이때 distribution과 alignment 속성을 잘못 설정하면 뷰가 왜곡되거나 예상치 못한 공백이 생기기 쉬워요. 특히 셀 내부나 하단 버튼 그룹처럼 크기가 유동적인 영역에서는 contentCompressionResistancePriority와 contentHuggingPriority를 명시적으로 조절해야 합니다. 기본값만 믿고 두면 콘텐츠가 너무 작게 줄어들거나 밀리는 현상이 발생하더라고요. 고정을 원하는 방향에는 widthAnchor나 heightAnchor로 NSLayoutConstraint를 명시적으로 추가하거나, 커스텀 뷰라면 intrinsicContentSize를 재정의하여 크기를 확정해야 합니다. 배열로 뷰를 관리한 뒤 필요할 때 removeFromSuperview 메서드를 호출하면 arrangedSubviews에서도 자동으로 제거되며 메모리 관리도 깔끔해지죠. 데이터 변동 시에도 layoutIfNeeded를 호출하면 즉시 재배치되며, 중첩된 StackView를 사용할 때는 축 방향이 직교할 필요 없이 같은 방향으로 쌓아도 전혀 무방해요. intrinsicContentSize가 명확한 컨트롤이라면 우선순위 조정 없이도 자연스럽게 크기가 결정되지만, 커스텀 뷰를 추가할 때는 이 프로퍼티를 override하여 정확한 값을 반환하는 것이 핵심이에요. 이렇게 하면 레이아웃 업데이트 시 계산 오차가 줄어들고 예상과 동일한 화면이 유지됩니다. 제약 조건 충돌 과정에서 conflicting constraint가 발생하면 디버거에서 빨간 줄로 확인할 수 있네요.

let stack = UIStackView()
stack.axis = .vertical
stack.distribution = .fillProportionally
stack.alignment = .leading

let data = [1, 2, 3]
for _ in data {
    let view = UIView(frame: .zero)
    view.backgroundColor = .systemGray5
    view.setContentCompressionResistancePriority(.required, for: .vertical)
    view.setContentHuggingPriority(.defaultLow, for: .vertical)
    
    stack.addArrangedSubview(view)
}

layoutMarginsGuide를 활용하면 여백을 일관되게 유지할 수 있으며, spacing 속성으로 뷰 간격도 코드 한 줄로 관리할 수 있어요. 동적 레이아웃은 데이터 변동에 유연하게 대응할 수 있는 기본기입니다. 우선순위 충돌만 피하면 프레임 계산 없이도 안정적인 화면 구성이 가능하죠.

iOS 앱을 만들다 보면 사용자 로그인 정보를 안전하게 보관해야 할 때가 자주 생기거든요. 예전에는 UserDefaults에 저장을 하곤 했는데, 암호화도 안 되어 있고 탈옥 환경에서는 쉽게 노출될 수 있어서 실무에서 절대 쓰면 안 된다는 걸 알고 계실 거예요. 그래서 저는 항상 키체인 접근 클래스를 사용해서 데이터를 저장하고 불러오는데요, 처음엔 API가 다소 복잡하게 느껴지기도 했지만 한 번 구조만 익혀놓으면 정말 편리하더라고요. 특히 인증 토큰이나 개인 설정값처럼 앱 재실행 시에도 반드시 유지해야 할 데이터는 필수로 거쳐야 하는 단계라고 생각해요.

import Foundation
import Security

class KeychainManager {
    static func save(_ data: Data, forKey key: String) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "com.example.app",
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]
        SecItemDelete(query as CFDictionary)
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw NSError(domain: "KeychainError", code: Int(status), userInfo: nil) }
    }

    static func load(forKey key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "com.example.app",
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess else { return nil }
        return result as? Data
    }
}

코드를 보면 알 수 있듯이 키체인 작업은 쿼리 딕셔너리를 구성하는 게 가장 중요해요. 서비스 식별자를 앱 고유 값으로 꼭 바꿔주셔야 하고, 기존에 저장된 항목이 있다면 SecItemDelete로 먼저 비워준 뒤 추가하는 방식이 안정적이더라고요. 데이터를 불러올 때는 kSecReturnData를 true로 설정해야 실제 바이트 배열이 반환되는데, 타입 캐스팅을 잘못하면 런타임 오류가 날 수 있으니 조심하셔야 해요. 저희 팀에서는 기기 간 동기화가 필요하지 않은 경우 kSecAttrAccessible을 WhenUnlocked으로 고정해서 설정해두었어요. 이렇게 하면 기기가 잠금 상태일 때만 키체인 접근이 차단되고, 앱이 백그라운드로 가도 정상적으로 읽을 수 있어요. 보안을 한층 더 견고하게 만들 수 있더라고요.

키체인은 앱이 삭제되더라도 자동으로 데이터가 지워지지 않는다는 특징이 있어서 오히려 재설치 시 이전 인증 토큰을 그대로 복원할 때 매우 유용하거든요. 대신 최초 실행 감지 후 잔존 데이터를 명시적으로 SecItemDelete로 정리해 주는 처리를 추가해야 깔끔하게 관리할 수 있어요. 앱 삭제 시 함께 지워지는 것은 UserDefaults나 샌드박스 파일 쪽이니 혼동하지 마시고요. 복잡한 API 호출 대신 위처럼 관리 클래스 하나만 만들어두시면 이후에 타입 안정성까지 챙기며 개발하실 때 정말 편하실 거예요. 이제부터 민감한 정보 처리는 키체인에 맡기시고 나머지는 앱 로직에 집중하시면 개발 속도가 훨씬 빨라지실 거예요.

iOS 앱을 만들다 보면 옵셔널(Optional) 때문에 앱이 갑자기 죽는 경험, 한 번쯤 있으실 거예요. 저도 처음엔 느낌표(!)로 강제 언래핑을 남발하다가 nil을 만나서 크래시나는 걸 여러 번 겪었거든요. 그래서 오늘은 옵셔널을 안전하게 푸는 방법을 정리해봤어요.

가장 기본은 if let이에요. 옵셔널에 값이 들어있을 때만 안쪽 블록을 실행하는 방식이라, nil이면 그냥 지나가거든요.

if let name = user.name { print("이름은 \(name)") }

Swift 5.7부터는 이름이 같으면 if let name 처럼 짧게 쓸 수도 있어서 훨씬 깔끔해졌더라고요.

두 번째는 guard let이에요. 이건 값이 없으면 아예 함수를 일찍 빠져나가는 용도라, 초반에 조건을 걸러내고 나머지 코드에서 계속 값을 쓸 때 좋아요. else 블록에서 꼭 return이나 throw로 현재 범위를 벗어나야 한다는 점만 기억하시면 돼요.

guard let name = user.name else { return }

세 번째는 nil 병합 연산자(??)예요. 값이 nil이면 기본값을 대신 넣어주는 건데, 한 줄로 끝나서 개인적으로 제일 자주 써요.

let name = user.name ?? "손님"

값 자체가 없어도 되는 상황이라면 옵셔널 체이닝(?.)도 편해요. user.profile?.age 처럼 중간이 nil이면 전체가 그냥 nil이 되고 크래시는 안 나거든요.

정리하면, 강제 언래핑(!)은 정말 nil이 절대 아니라는 확신이 있을 때만 쓰고, 평소엔 if let / guard let / ?? 조합으로 가는 걸 추천해요. 저는 이 습관을 들이고 나서 크래시가 확 줄더라고요.

출처: Swift 공식 문서(The Swift Programming Language) - Optionals https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/

'develop > iOS' 카테고리의 다른 글

UIStackView 동적 뷰 추가 및 제약 조건 처리  (0) 2026.07.22
키체인 인증 정보 저장 구현  (0) 2026.07.16
ios 탈옥폰 체크(swift)  (0) 2020.04.23
ios 탈옥폰 체크  (0) 2019.12.12
swift 타입체크 함수 (UIViewController)  (0) 2019.12.11

참조: https://6developer.com/935

object-c: https://indra17.tistory.com/entry/ios-%ED%83%88%EC%98%A5%ED%8F%B0-%EC%B2%B4%ED%81%AC?category=533814

func hasJailbreak() -> Bool {
        guard let cydiaUrlScheme = NSURL(string: "cydia://package/com.example.package") else { return false }
        if UIApplication.shared.canOpenURL(cydiaUrlScheme as URL) {
            return true
        }
        #if arch(i386) || arch(x86_64)
        return false
        #endif
        
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: "/Applications/Cydia.app") ||
            fileManager.fileExists(atPath: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
            fileManager.fileExists(atPath: "/bin/bash") ||
            fileManager.fileExists(atPath: "/usr/sbin/sshd") ||
            fileManager.fileExists(atPath: "/etc/apt") ||
            fileManager.fileExists(atPath: "/usr/bin/ssh") ||
            fileManager.fileExists(atPath: "/private/var/lib/apt") {
            return true
        }
        if canOpen(path: "/Applications/Cydia.app") ||
            canOpen(path: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
            canOpen(path: "/bin/bash") ||
            canOpen(path: "/usr/sbin/sshd") ||
            canOpen(path: "/etc/apt") ||
            canOpen(path: "/usr/bin/ssh") {
            return true
        }
        let path = "/private/" + NSUUID().uuidString
        do {
            try "anyString".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
            try fileManager.removeItem(atPath: path)
            return true
        } catch {
            return false
        }
    }
    func canOpen(path: String) -> Bool {
        let file = fopen(path, "r")
        guard file != nil else { return false }
        fclose(file)
        return true
    }

출처: http://bitxflow.synology.me/wordpress/?p=311%20

swift: https://indra17.tistory.com/entry/ios-%ED%83%88%EC%98%A5%ED%8F%B0-%EC%B2%B4%ED%81%ACswift

+(BOOL)isJailbroken{
#if !(TARGET_IPHONE_SIMULATOR)
    if ([[NSFileManager defaultManager] fileExistsAtPath:@”/Applications/Cydia.app”]){
        return YES;
    }else if([[NSFileManager defaultManager] fileExistsAtPath:@”/Library/MobileSubstrate/MobileSubstrate.dylib”]){
        return YES;
    }else if([[NSFileManager defaultManager] fileExistsAtPath:@”/bin/bash”]){
        return YES;
    }else if([[NSFileManager defaultManager] fileExistsAtPath:@”/usr/sbin/sshd”]){
        return YES;
    }else if([[NSFileManager defaultManager] fileExistsAtPath:@”/etc/apt”]){
        return YES;
    }
    return NO;

    NSError *error;
    NSString *stringToBeWritten = @"This is a test.";
    [stringToBeWritten writeToFile:@”/private/jailbreak.txt” atomically:YES encoding:NSUTF8StringEncoding error:&error];
    if(error==nil){
        //Device is jailbroken
        return YES;
    } else {
        //Device is not jailbroken
        [[NSFileManager defaultManager] removeItemAtPath:@”/private/jailbreak.txt” error:nil];
    }
    if([[UIApplication sharedApplication] canOpenUrl: [NSURL URLWithString:@”cydia://package/com.example.package”]]){
        //Device is jailbroken
    }
#endif
  
    //All checks have failed. Most probably, the device is not jailbroken
    return NO;
}

함수 정의,  UIViewController대신에 원하는 타입의 부모클래스를 넣으면 됨

func isAleadyVC<T>(_ vcs:[UIViewController]?, _ tp: T.Type) -> UIViewController? {
	for i in 0 ..< vcs!.count {
		if (vcs![i] as? UINavigationController) != nil {
			let nc = vcs![i] as! UINavigationController
			if let vc = isAleadyVC(nc.children, tp) {
				return vc
			}
		}
		else if (vcs![i] as? UITabBarController) != nil {
			let tc = vcs![i] as! UITabBarController
			if let vc = isAleadyVC(tc.children, tp) {
				return vc
			}
		}
		else { // always UIViewController
			let vc = vcs![i]
			if type(of: vc) == tp {
				return vc
			}
			if let vc = isAleadyVC(vc.children, tp) {
				return vc
			}
		}
	}
	return nil
}

 

사용

if let homeVC = isAleadyVC(self.window?.rootViewController?.children, HomeVC.self) as? HomeVC {
	...
}

'develop > iOS' 카테고리의 다른 글

ios 탈옥폰 체크(swift)  (0) 2020.04.23
ios 탈옥폰 체크  (0) 2019.12.12
xcode에서 개발/운영 환경 설정  (0) 2019.03.28
swift에서 macro define 사용하기  (0) 2019.03.28
swift용 SQLite 클래스  (0) 2018.07.13

프로젝트 생성 시 기본적으로 Debug, Release Scheme이 생성되어 있다.

Scheme설정 중 Archive는 Release, Run은 Debug가 기본 설정

 

1. 프로젝트 설정에서 개발, 운영 용 의 설정셋을 추가한다 (Debug설정으로 복사)

 

2. Scheme을 추가한다

 

3. scheme과 설정셋을 매핑

 

4. 개발/운영 설정을 설정셋에 추가한다

프로젝트 설정 - 타겟 - Build Settings - Active Compilation Conditions 의 개발/운영 환경에 macro define값을 추가 (여기선 dev만 구분)

 

5. 소스에서 macro define으로 구분

#if DEV

public static let DOMAIN = "http://개발서버주소"

#else

public static let DOMAIN = "http://운영서버주소"

#endif

 

6. 추가적으로 info.plist파일 복제하여 개발/운영에 따라 앱 이름, bundle ID 등을 변경

'develop > iOS' 카테고리의 다른 글

ios 탈옥폰 체크  (0) 2019.12.12
swift 타입체크 함수 (UIViewController)  (0) 2019.12.11
swift에서 macro define 사용하기  (0) 2019.03.28
swift용 SQLite 클래스  (0) 2018.07.13
ipa 커맨드라인으로 만들기 (xcode 9.2 대응)  (0) 2018.04.09

#ifdefine 은 사용할 수 없다

 

#if, #else, #endif를 사용

+ Recent posts