2024년 11월 8일

코딩 - ChatGPT 을 이용한 맥 프로그램 만들기(오늘의 명언)

ChatGPT 의 코딩 실력을 확인하는 목적으로 간단한 맥용 프로그램을 만들어 보았다. 참고로 맥 프로그래밍은 과거에 관련 서적을 보고 Object C 기반의 웹뷰 예제를 만들어 본것이 전부이다. 

◼︎ 개발 환경
  • HW : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 15.0.1 (24A348)
  • TOOLS : XCode 16.1
  • Programming Language : Shift

◼︎ 프로그램 요구사항
  • 배경 이미지와 명언이 프로그램을 클릭하면 변경되는 간단한 오늘의 명언 프로그램.
  • 간단한 날씨정보도 같이 보여줌.
참고로 ChatGPT 는 유료버전의 4o을 사용했다.

어떻게 프로그램을 만들 것인가 알아보기

🆀 맥에서 동작하는 Unsplash 에서 랜덤 이미지를 배경으로 명언 보여주는 프로그램을 스위프트를 사용하여 만들고 싶다. 화면을 클릭하면 매번 배경이미지와 명언을 새로 보여주고 싶다.
 
🅰 이 프로그램은 Xcode에서 Swift와 Cocoa 프레임워크를 사용해 개발할 수 있으며, 네트워크 요청을 통해 Unsplash API에서 이미지를 가져오고 화면에 표시하는 방식입니다.

요약 후 아래와 같은 절차를 가이드 한다.
  1. Unsplash API 키 준비 
  2. XCode 을 설치
  3. 새로운 App 프로젝트를 생성
  4. 예제 코드 
명언 역시 외부에서 가져오고 싶어 아래와 같은 질문을 했다.

🆀 명언도 API 을 사용하여 랜덤하게 가져오려면

🅰 명언을 API에서 랜덤하게 가져오려면 다양한 명언 API를 사용할 수 있습니다. 예를 들어, Quotes REST API나 ZenQuotes API를 사용하면 간편하게 랜덤 명언을 가져올 수 있습니다. 여기서는 ZenQuotes API를 사용하여 Swift 코드에 적용하는 방법을 설명드리겠습니다.

요약 후 앞의 코드에 명언을 ZenQuotes API 사용하여 가져오는 수정된 코드를 제시한다. (참고로 ZenQuotes API 는 API Key 없이 사용할 수 있다.)


프로젝트 생성

ChatGPT 가이드에 따라 XCode 을 사용하여 새로운 "오늘의 명언" 프로젝트를 아래 절차로 생성하였다.

그림1. 새로운 프로젝트 생성
그림1. 새로운 프로젝트 생성
그림2. 템플릿 목록에서 App 선택
그림3. 프로젝트 정보 입력 및 생성
(생성시 Testing System 을 None 로 변경하여 테스트를 위한 프로그램들이 생성되지 않도록 했다.)

XCode 에 의하여 생성된 프로젝트는 아래과 같은 파일로 구성된다. (프로젝트 이름을 Quotes 로 한 경우)

그림4. 생성된 프로젝트 파일 구조

Unsplash API 사용을 위해서는 API Key 가 필요하다. API Key 는 Unsplash 홈페이지 에서 회원 가입 후 Unsplash Developer 페이지로 이동해서 Your Apps 섹션에서 New Application 버튼을 클릭하여 새로운 앱을 만드는 것으로 키를 생성할 수 있다.

"오늘의 명언" 프로그램 코딩

ContentView 소스는 ChatGPT 가 제시한 코드를 적용하였다.
빌드하고 실행하면 통신 오류 처럼 보여지는 에러를 확인 할 수 있는데 이 부분을 해결하는 것에 큰 어려움이 있었다.


Error fetching quote: Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found."

ChatGPT 는 위 오류에 대하여 (앱과 서버 간의 통신을 HTTPS를 통한 암호화된 연결로 제한하는) ⑴ATS 설정을 추가하거나 ⑵서버 API 상태방화벽 점검을 해결방법을 제안하였다. 외부 서비스가 https 을 사용하는 이유로 ATS 문제는 아니고 방화벽 과 같은 네트워크 문제도 아닌것으로 확신하여 프로젝트 설정에 보안 관련 사항이 있는 것은 아닌가를 의심하고 아래와 같은 추가 질문을 했다.

 ATS(App Transport Security) 는  Apple이 iOS 9 및 macOS 10.11(El Capitan)부터 앱의 네트워크 보안을 강화하기 위해 도입한 기술이다. ATS는 앱이 네트워크를 통해 데이터를 전송할 때 보안이 강화된 HTTPS 연결을 사용하도록 요구하며 이를 통해 중간자 공격, 데이터 탈취 등의 위험을 줄이고 네트워크 보안 표준을 준수하도록 한다. 

🆀 맥용 앱 프로젝트는 보안 관련 고려하여야 할 제약이 있는가

 























답변 내용중에서 App Sandbox 관련 이슈라는 확신을 갖고 아래와 같은 질문을 했다.

🆀 App Sandbox 에서 Connections 등의 세부 권한을 추가 하려면

답변에 따라 프로젝트의 Xcode에서 프로젝트 파일을 클릭하고, Targets에서 앱의 타겟을 선택한 후 Signing & Capabilities 탭으로 이동 Outgoing Connections 항목을 체크 하여 문제를 해결하였다.


이제 프로그램을 실행하면 아래와 같다.


 App Sandbox 는  Apple은 보안을 강화하고 앱이 사용자 데이터를 오용하거나 시스템 자원에 무단으로 접근하는 것을 방지하기 위해 macOS Lion (10.7) 버전부터 App Sandbox를 도입했다. 특히, Mac App Store를 통해 배포되는 앱은 사용자 신뢰와 보안을 보장해야 했기 때문에 강제화되었다. 주요한 특징으로 격리실행(앱은 자신의 데이터와 권한 내에서만 작동하며, 다른 앱이나 시스템에 접근할 수 없음), 권한 기반 접근(파일, 네트워크, 하드웨어 리소스 등 필요한 권한만 명시적으로 요청), 보안강화(악성 코드 실행 및 데이터 탈취 방지) 을 가지고 있다.

"오늘의 명언" 프로그램 개선하기

프로그램을 개선을 위하여 아래의 기능들을 추가로 정의하였다. 
  1. 무료 버전의 Unsplash API  호출은 시간당 50개의 요청과 하루에 5,000개의 요청으로 제약이 있다. 이런 이유에서 이미지를 가져오지 못하는 경우 미리 가지고 있는 이미지를 랜덤으로 보여주는 기능이 구현.
  2. 다음으로 단순하게 텍스트를 보여주는 것이 아니라 타이핑과 같은 효과 구현
  3. 외부에서 이미지를 가져오는 Unsplash API 을 호출하려면 키가 필요한데 소스에 코딩하여 사용하던 것을 입력할 수 있도록 프로그램 환경설정에 메뉴를 추가
  4. 우측 상단에 위치정보를 기반으로 테스트로 위치와 날씨 노출 구현
  5. Unsplash 의 경우 좌측 하단에 인스타그램과 같이 제작자 정보 노출 구현
기능 구현을 위하여 아래와 같은 질문들을 했다.

🆀 Unsplash API 을 통하여 이미지를 가져오지 못할 때는 디폴트 이미지들을 랜덤으로 보여주게 하려면
🅰 Unsplash API를 통해 이미지를 가져오지 못할 때, 로컬에 저장된 기본 이미지를 랜덤으로 표시하는 기능을 추가할 수 있습니다. 이 기능을 구현하려면 이미지 로드 실패 시 로컬 이미지 배열에서 무작위로 선택하여 표시하는 로직을 작성하면 됩니다.


    
private func fetchRandomLocalImage() {

        let imageNames = (1...8).map { "\($0)" }

        guard let randomImageName = imageNames.randomElement(), let nsImage = NSImage(named: randomImageName) else {

            print("Error: Could not load local image.")

            self.image = nil

            return

        }

        self.isUnsplashImage = false // Unsplash 이미지가 아님을 설정

        self.image = nsImage

        self.fetchRandomQuote()

    }



제공되는 예제를 적용하여 Unsplash 에서 이미지를 가져오는 과정에서 오류가 발생하면 가지고 있는 이미지들 중에서 랜덤하게 하나의 이미지를 가져와 배경으로 보여주도록 기능을 추가하였다. 이미지는 AI 이미지 관련 웹 사이트에서 몇개를 다운로드하여 사용하였다. 로컬 이미지 추가는  Assets 에 새로운 폴더 "Images" 를 생성하고 이미지를 Drag&Drop 하여 이미지를 추가 하고 코딩시 이미지 목록을 자동화 하여 가져올 수 있도록 이름을 1, 2, 3, 4 형태로 변경하여 사용하였다. 


🆀 명언 텍스트를 보여줄때 타이핑 효과를 주려면
🅰 SwiftUI에서 텍스트에 타이핑 효과를 주려면 애니메이션과 타이머를 조합하여 텍스트가 한 글자씩 나타나도록 구현할 수 있습니다. DispatchQueue와 타이머를 사용해 문자열의 각 문자를 일정 간격으로 추가하여 타이핑 애니메이션을 만들 수 있습니다.

제공되는 예제를 적용하여 문자를 티이핑하는 효과를 구현하는 기능의 TypewriterText 구조체를 추가하였다.

struct TypewriterText: View {

    var text: String

    var onComplete: (() -> Void)? = nil // 타입 효과 완료 시 호출되는 클로저

    @State private var displayedText: String = ""

    @State private var currentIndex: Int = 0

    @State private var typingTimer: Timer? = nil

    private let typingSpeed = 0.07


    var body: some View {

        Text(displayedText)

            .onAppear {

                startTyping()

            }

            .onChange(of: text) {

                resetTyping()

                startTyping()

            }

    }

    

    private func startTyping() {

        typingTimer?.invalidate()

        typingTimer = nil

        displayedText = ""

        currentIndex = 0

        

        typingTimer = Timer.scheduledTimer(withTimeInterval: typingSpeed, repeats: true) { timer in

            if currentIndex < text.count {

                let index = text.index(text.startIndex, offsetBy: currentIndex)

                displayedText.append(text[index])

                currentIndex += 1

            } else {

                timer.invalidate()

                typingTimer = nil

                onComplete?() // 타이핑이 완료되면 onComplete 클로저 호출

            }

        }

    }


    private func resetTyping() {

        typingTimer?.invalidate()

        typingTimer = nil

        displayedText = ""

        currentIndex = 0

    }

}


🆀 프로그램 환경설정에 Unsplash API  키를 설정할 수 있게 하려면
🅰 프로그램의 환경설정 창에서 API 키를 설정할 수 있도록 하려면 SwiftUI 앱에서는 Settings를 사용하여 시스템 메뉴의 기본 설정 창에 해당 UI를 추가할 수 있습니다. 이를 통해 Unsplash API Key를 설정할 수 있도록 합니다.

제공되는 예제를 적용하여 키를 설정하는 기능의 SettingsView 구조체를 추가하였다.

import SwiftUI


struct SettingsView: View {

    

    @AppStorage("UnsplashAPIKey") private var unsplashApiKey: String = ""

    @AppStorage("OpenWeatherMapAPIKey") private var weatherApiKey: String = ""

    @AppStorage("OpenAIAPIKey") private var openAIAPIKey: String = ""

        

    @State private var showSaveConfirmation = false


    var body: some View {

        VStack(spacing: 20) {

            // API Key Table

            VStack(alignment: .leading, spacing: 15) {

                HStack {

                    Text("Unsplash API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter Unsplash API Key", text: $unsplashApiKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }


                HStack {

                    Text("OpenWeatherMap API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter OpenWeatherMap API Key", text: $weatherApiKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }

                HStack {

                    Text("OpenAI API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter OpenAI API Key", text: $openAIAPIKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }

            }

            .padding(.horizontal, 20)

            .padding(.top, 40)

            

            Spacer()


            // Save Button with Clear Background

            Button(action: {

                showSaveConfirmation = true

            }) {

                Text("Save Changes")

                    .font(.headline)

                    .foregroundColor(.blue) // Text color only, no background color

                    .padding()

                    .frame(maxWidth: .infinity)

                    .overlay(

                        RoundedRectangle(cornerRadius: 8)

                            .stroke(Color.blue, lineWidth: 2)

                    )

            }

            .buttonStyle(PlainButtonStyle()) // Ensures the button has no additional background styling

            .padding([.leading, .trailing, .bottom], 20)

            .alert(isPresented: $showSaveConfirmation) {

                Alert(title: Text("Settings Saved"), message: Text("Your API keys have been saved."), dismissButton: .default(Text("OK")))

            }

        }

        .frame(width: 500, height: 250)

    }

}


🆀 사용자 위치정보를 기반으로 오늘의 날씨 정보와 위치 정보를 우측 상단 고정된 영역에 보려주려면
🅰 사용자의 위치 정보를 기반으로 날씨와 위치를 우측 상단에 고정하여 보여주는 방법을 구현할 수 있습니다. 아래 예제에서는 LocationManager를 통해 사용자의 위치를 가져오고, OpenWeatherMap API를 사용하여 날씨 정보를 표시합니다.

제공되는 예제를 적용하여 WeatherView 구조체를 추가하였다.

struct WeatherView: View {

    @Binding var locationString: String?

    let weatherInfo: String


    var body: some View {

        VStack(alignment: .trailing) {

            Text(weatherInfo)

                .font(.subheadline)

                .foregroundColor(.white)

                .padding()

                .background(Color.black.opacity(0.4))

                .cornerRadius(8)

                .padding(.trailing, 20)

        }

        .padding(.top, 40)

    }

}


class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {

    private let manager = CLLocationManager()

    @Published var userLocation: CLLocationCoordinate2D?

    @Published var locationString: String?


    override init() {

        super.init()

        manager.delegate = self

        manager.desiredAccuracy = kCLLocationAccuracyBest

    }


    func requestLocation() {

        manager.requestWhenInUseAuthorization()

        manager.requestLocation()

    }


    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        if let location = locations.first {

            userLocation = location.coordinate

            locationString = "Lat: \(location.coordinate.latitude), Lon: \(location.coordinate.longitude)"

        }

    }


    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {

        print("Location error: \(error.localizedDescription)")

    }

}


🆀 Unsplash API 에서 가져온 이미지를 보여줄 때는 인스타그램과 같이 이미지 이름과 작성자 그리고 프로필 이미지를 하단 좌측에 노출하게 하면
🅰 Unsplash API에서 이미지를 가져올 때, 이미지 정보와 작성자 이름, 작성자 프로필 사진 URL 등을 함께 받아와 화면에 표시할 수 있도록 코드를 수정할 수 있습니다. 아래는 해당 기능을 구현한 코드입니다. 이 코드는 이미지 하단 좌측에 Unsplash에서 가져온 이미지 이름과 작성자 정보를 표시합니다.

제공되는 예제를 적용하여 코드를 수정하였다.

        private func fetchImageFromUnsplash() {

            guard !unsplashApiKey.isEmpty, let url = URL(string: "https://api.unsplash.com/photos/random?client_id=\(unsplashApiKey)") else {

                print("Error: Unsplash API Key is missing or invalid URL.")

                fetchRandomLocalImage()

                return

            }

            

            URLSession.shared.dataTask(with: url) { data, response, error in

                defer { isLoading = false }

                

                if let error = error {

                    print("Error fetching image: \(error.localizedDescription)")

                    DispatchQueue.main.async {

                        self.fetchRandomLocalImage()

                    }

                    return

                }

                

                guard let data = data else {

                    print("Error: No data received.")

                    DispatchQueue.main.async {

                        self.fetchRandomLocalImage()

                    }

                    return

                }

                

                do {

                    if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],

                       let urls = json["urls"] as? [String: String],

                       let imageUrlString = urls["regular"],

                       let imageUrl = URL(string: imageUrlString),

                       let user = json["user"] as? [String: Any],

                       let authorName = user["name"] as? String,

                       let profileImageUrls = user["profile_image"] as? [String: String],

                       let profileImageUrlString = profileImageUrls["small"],

                       let profileImageUrl = URL(string: profileImageUrlString),

                       let description = json["description"] as? String {

                        

                        self.imageTitle = description

                        self.authorName = authorName

                        self.isUnsplashImage = true // Unsplash 이미지임을 설정

                        DispatchQueue.main.async {

                            self.downloadImage(from: imageUrl)

                            self.downloadAuthorProfileImage(from: profileImageUrl)

                        }

                    } else {

                        print("Error: Could not parse JSON.")

                        DispatchQueue.main.async {

                            self.fetchRandomLocalImage()

                        }

                    }

                } catch {

                    print("Error parsing JSON: \(error.localizedDescription)")

                    DispatchQueue.main.async {

                        self.fetchRandomLocalImage()

                    }

                }

            }.resume()

        }

        

        private func downloadImage(from url: URL) {

            URLSession.shared.dataTask(with: url) { data, response, error in

                guard let data = data, let nsImage = NSImage(data: data) else {

                    if let error = error {

                        print("Error downloading image: \(error.localizedDescription)")

                    } else {

                        print("Error: Unable to create image from data.")

                    }

                    DispatchQueue.main.async {

                        self.fetchRandomLocalImage()

                    }

                    return

                }

                

                DispatchQueue.main.async {

                    self.image = nsImage

                    self.fetchRandomQuote() // 이미지가 로드된 후에 명언을 가져옴

                }

            }.resume()

        }

        

        private func downloadAuthorProfileImage(from url: URL) {

            URLSession.shared.dataTask(with: url) { data, response, error in

                guard let data = data, let nsImage = NSImage(data: data) else {

                    print("Error downloading author profile image: \(error?.localizedDescription ?? "Unknown error")")

                    return

                }

                

                DispatchQueue.main.async {

                    self.authorProfileImage = nsImage

                }

            }.resume()

        }

    


🆀 로컬 이미지의 경우에는 작성자 정보를 보이지 않고 싶은데
🅰 로컬 이미지를 사용할 때는 작성자 정보가 표시되지 않도록 설정할 수 있습니다. 이를 위해, Unsplash에서 이미지를 가져온 경우에만 작성자 정보가 표시되도록 조건을 추가하면 됩니다. ContentView에서 Unsplash 이미지를 사용할 때만 작성자 정보를 보여주는 플래그를 추가하겠습니다.

제공되는 예제를 적용하여 코드를 수정하였다.

🆀 명언이 영어로 제공되는데 chatgpt API 을 사용하여 한국어로 같이 표시하려면

🅰 ChatGPT API를 사용하여 영어로 제공된 명언을 한국어로 번역하는 방법을 구현할 수 있습니다. 아래는 OpenAI의 ChatGPT API를 통해 영어 명언을 번역하여 한국어와 영어로 함께 표시하는 예제 코드입니다. 이 코드는 Swift에서 URLSession을 사용하여 ChatGPT API를 호출하고 번역 결과를 받아와 표시합니다.

제공되는 예제를 적용하여 코드를 수정하였다. 

func translateQuoteToKorean(_ quote: String) {

        guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {

            print("Error: Invalid URL for OpenAI API")

            return

        }

        

        var request = URLRequest(url: url)

        request.httpMethod = "POST"

        request.addValue("Bearer \(openAIAPIKey)", forHTTPHeaderField: "Authorization")

        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        

        let prompt = "Translate this quote to Korean: \(quote)"

        let parameters: [String: Any] = [

            "model": "gpt-4o-mini",

            "messages": [

                ["role": "user", "content": prompt ]

            ],

            "max_tokens": 60

        ]

        

        do {

            request.httpBody = try JSONSerialization.data(withJSONObject: parameters)

        } catch {

            print("Error: Failed to serialize JSON for request body - \(error)")

            return

        }

        

        print("Sending request to ChatGPT API with prompt: \(prompt)")

        

        URLSession.shared.dataTask(with: request) { data, response, error in

            if let error = error {

                print("Error: Network request failed - \(error.localizedDescription)")

                return

            }

            

            if let httpResponse = response as? HTTPURLResponse {

                print("Received HTTP response: \(httpResponse.statusCode)")

            }

            

            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 429 {

                    print("Quota exceeded. Please check your API plan and usage.")

                    // 사용자에게 할당량 초과 메시지를 표시하는 로직 추가

                    //return

            }

            

            guard let data = data else {

                print("Error: No data received from ChatGPT API")

                return

            }

            

            do {

                if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {

                    print("Received JSON response: \(json)")

                    

                    if let choices = json["choices"] as? [[String: Any]],

                       let message = choices.first?["message"] as? [String: Any],

                       let translatedText = message["content"] as? String {

                        

                        DispatchQueue.main.async {

                            self.translatedQuote = translatedText.trimmingCharacters(in: .whitespacesAndNewlines)

                        }

                    } else {

                        print("Error: Unexpected JSON structure or missing 'choices' key")

                    }

                }

            } catch {

                print("Error: Failed to parse JSON response - \(error)")

            }

        }.resume()

    }


추가로 앞선 SettingsView 구조체에 ChatGPT API 키 설정 코드를 추가하고 이 키값을 사용하도록 ContentView 를 수정하였다.

import SwiftUI


struct SettingsView: View {

    

    @AppStorage("UnsplashAPIKey") private var unsplashApiKey: String = ""

    @AppStorage("OpenWeatherMapAPIKey") private var weatherApiKey: String = ""

    @AppStorage("OpenAIAPIKey") private var openAIAPIKey: String = ""

        

    @State private var showSaveConfirmation = false


    var body: some View {

        VStack(spacing: 20) {

            // API Key Table

            VStack(alignment: .leading, spacing: 15) {

                HStack {

                    Text("Unsplash API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter Unsplash API Key", text: $unsplashApiKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }


                HStack {

                    Text("OpenWeatherMap API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter OpenWeatherMap API Key", text: $weatherApiKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }

                HStack {

                    Text("OpenAI API Key")

                        .frame(width: 150, alignment: .leading)

                    TextField("Enter OpenAI API Key", text: $openAIAPIKey)

                        .textFieldStyle(RoundedBorderTextFieldStyle())

                        .frame(width: 300)

                }

            }

            .padding(.horizontal, 20)

            .padding(.top, 40)

            

            Spacer()


            // Save Button with Clear Background

            Button(action: {

                showSaveConfirmation = true

            }) {

                Text("Save Changes")

                    .font(.headline)

                    .foregroundColor(.blue) // Text color only, no background color

                    .padding()

                    .frame(maxWidth: .infinity)

                    .overlay(

                        RoundedRectangle(cornerRadius: 8)

                            .stroke(Color.blue, lineWidth: 2)

                    )

            }

            .buttonStyle(PlainButtonStyle()) // Ensures the button has no additional background styling

            .padding([.leading, .trailing, .bottom], 20)

            .alert(isPresented: $showSaveConfirmation) {

                Alert(title: Text("Settings Saved"), message: Text("Your API keys have been saved."), dismissButton: .default(Text("OK")))

            }

        }

        .frame(width: 500, height: 250)

    }

}


ChatGPT API 사용을 위해서는 Access Key 가 필요하다. Access Key 는 OpenAI 홈페이지에서 계정을 생성하거나 기존 계정으로 로그인 후 API 키 생성 페이지 로 이동 "Create new secret key" 버튼을 클릭하여 키를 생성할 수 있다. 






총평

기대 이상의 정확한 설명과 예제 코드는 인상적이다. 중간 중간 오류가 발생하기도 했지만 관련하여 문의하면 또 적절한 답변을 해주었다. 

ChatGPT 와 대화 가운데 잘모르는 것은 바로 질문하여 확인할 수 있기 때문에 자연스럽게 학습이 되었다.

충분한 맥 코딩 경험과 언어를 잘알고 있다면 전문가수준의 코드 작성도 충분하게 가능할것 같다는 느낌이다.