SwiftUI와 GraphQL을 사용한 Instagram 클론
오늘은 Instagram과 유사한 애플리케이션인 나만의 소셜 네트워크를 구축하기 위해 여러 가지 멋진 도구를 사용하는 방법을 알려드리는 블로그 포스팅 시리즈를 시작하려고 합니다.
우리는 기술을 아끼지 않고 최신의 최고급 도구를 사용할 것입니다: Parse, GraphQL, 일부 NodeJS, 특히 (아직 출시되지 않은) 최신 Apple의 프레임워크 SwiftUI를 사용할 것입니다.
완전한 기능을 갖추려면 몇 번의 포스팅이 필요하겠지만, Back4app에서 아주 적은 노력으로 아이디어를 실행하는 것이 얼마나 간단한지 알게 될 것입니다.
더 나은 학습을 위해 소스 코드가 포함된 iOS Instagram 클론 프로젝트를 다운로드하세요.
자, 이제…
Contents
빠르게 시작하고 싶으신가요?
허브에서 이 앱을 복제하여 번거로움 없이 바로 사용해보세요!
약간의 소개
Parse가 개발 프로세스에 가져다주는 엄청난 이점, 특히 Back4app에서 호스팅하는 경우 생산성을 크게 향상시키는 도구 세트에 대해 이미 알고 계실 것입니다.
그리고 GraphQL에 대한 최근 포스팅을 통해 이를 사용하면 시간이 지나도 API 개발을 얼마나 쉽게 유지 관리할 수 있는지 살펴보셨을 것입니다. 해당 포스팅을 놓치셨다면 시간을 내서 읽어보시면 이해도가 크게 향상될 것입니다.
GraphQL에 대한 자세한 내용은 웹 API 문서에서 확인할 수 있으며 사용 방법에 대한 전체 게시물도 있습니다.
또한 클라우드 코드 함수에서 GraphQL 및 NodeJS 읽기에서 NodeJS와의 일부 통합을 볼 수 있습니다.
이제 SwiftUI가 남았습니다.
Apple에 따르면 SwiftUI는 “Swift의 강력한 기능으로 모든 Apple 플랫폼에서 사용자 인터페이스를 구축하는 혁신적이고 매우 간단한 방법”이라고 합니다.
저는 SwiftUI가 그 이상이라고 생각합니다.
저처럼 오랫동안 소프트웨어를 개발해 온 개발자라면 개발자 커뮤니티가 스토리보드의 편리함, Apple의 드래그 앤 드롭 UI 구축 인터페이스 또는 프로그래매틱 UI의 강력함 사이에서 항상 분열되어 있다는 것을 알고 계실 것입니다.
SwiftUI에서는 초보자도 쉽게 배울 수 있으면서도 코딩된 UI의 유지보수성을 유지할 수 있는 두 가지 장점을 모두 제공합니다.
여기에 실시간 렌더링을 결합하여 개발자가 코딩하는 내용의 시각적 결과물을 쉽게 확인할 수 있고, 코드를 변경하면 UI에 반영되고, 그래픽으로 UI를 변경하면 코드에 반영되도록 양방향으로 만들면 아름답게 디자인하면서도 시간이 지나도 유지 관리가 가능한 UI를 제공하는 가장 강력하면서도 쉬운 방법 중 하나가 탄생합니다.
그렇다면 무엇이 필요할까요?
이 글을 작성하는 시점에 Apple은 새로운 macOS Catalina, XCode 11, iOS 13 및 곧 출시될 몇 가지 다른 제품을 베타 테스트 중입니다.
macOS Mojave와 함께 XCode 11을 사용할 수 있지만, SwiftUI의 미리보기를 실시간으로 렌더링하려면 Catalina가 필요합니다.
Catalina가 없으신가요? 프리뷰가 없습니다!
Catalina에서 공식 macOS 셸을 Bash에서 ZSH로 변경하면 일상적인 워크플로에 사용하는 많은 도구와 스크립트가 깨지는 것을 발견했기 때문에 당분간 Mojave를 계속 사용하기로 결정했습니다. 업데이트 여부는 사용자가 결정할 수 있습니다.
또한 앱을 만든 Back4app 계정이 필요합니다. Back4app 설명서에서 첫 번째 앱을 만드는 방법을 배울 수 있습니다.
마지막으로, GraphQL을 프로젝트에 통합하기 위해 Apollo 클라이언트를 사용할 것입니다. 경험이 없는 경우 웹 인증 구성 및 사용 방법에 대한 매우 자세한 게시물을 작성했습니다.
따라서 시작하기 전에 다음이 필요합니다:
- XCode 11 설치(미리 보기를 보려면 macOS Catalina 설치)
- Back4app에서 생성한 새 앱
- Apollo 클라이언트 설치 및 구성
내 앱은 Instagram과 유사하므로 Back4Gram이라고 부를 것입니다.
첫 번째 수업
저는 항상 Parse가 새로운 사용자를 등록하고 기존 사용자를 인증할 수 있는 사용자 클래스와 비슷한 권한을 가진 사용자를 그룹화할 수 있는 역할 클래스를 생성하는 방법에 대해 설명합니다. 적어도 인증의 경우 클래스를 만들 필요가 없으므로 매우 편리합니다.
User 클래스에는 자동으로 생성되는 세 가지 속성이 있는데, 그 중 두 개는 필수이고 하나는 선택 사항입니다:
- 사용자 이름(필수)
- 비밀번호(필수)
- 이메일(선택 사항)
이 정도면 앱에서 사용자를 등록하고 관리하기에 충분합니다.
아시다시피 사용자 이메일 저장에 대한 Apple의 정책은 매우 엄격하므로 해당 정보를 요청하거나 보관하지 않는 것이 좋으며, 그렇지 않으면 승인을 받지 못할 수도 있습니다.
이 글은 튜토리얼에 불과하므로 사용자가 어떻게 작동하는지 확인할 수 있도록 해당 정보를 입력하도록 요청하겠습니다.
자동으로 생성되는 사용자 및 역할 클래스
그다지 도움이 되지 않는 것처럼, 새로 만든 앱에는 이미 GraphQL 쿼리 및 변이가 자동으로 생성되어 있습니다. 이는 자동으로 생성된 클래스뿐만 아니라 시간이 지남에 따라 생성하는 다른 클래스에도 적용됩니다. 문서에도 설명되어 있습니다!
자동 완성 기능도 있다고 말씀드렸나요? 네, 있습니다!
따라서 첫 번째 변이는 위와 같으며, 특정 변이 signUp()을 사용하여 새 사용자를 생성합니다:
쿼리는 사용 중인 Parse 버전에 따라 달라집니다:
Parse 3.7.2:
mutation signUpUser($username: String!, $password: String!, $email: String) { users { signUp( fields: { 사용자 이름: $username, 비밀번호: $password, 이메일: 이메일 } ) { objectId } } }
Parse 3.8:
변이 signUpUser($username: String!, $password: String!, $email: String) { signUp( fields: { username: $username, password: $password, email: 이메일 } ) { objectId } }
Parse 3.9:
변이 signUpUser($username: String!, $password: String!, $email: String) { signUp( fields: { username: $username, password: $password, email: 이메일 } ) { id } }
사용자 이름과 비밀번호 변수는 유형 끝에 “!”를 추가하여 필수로 설정하고, 이메일은 유형 뒤에 “!”를 추가하지 않음으로써 선택 사항으로 설정하고 있음을 알 수 있습니다.
결국 새로 생성된 사용자의 objectId를 반환하고 있습니다.
여기까지 왔으니 이미 존재하는 사용자를 로그인하기 위한 GraphQL 변형을 코딩해 보겠습니다.
이 경우 사용자 이름과 비밀번호는 모두 필수이며, 사용자에 대한 세션 토큰을 검색하겠습니다:
Parse 3.7.2
변이 로그인유저($username: 문자열!, $password: 문자열!){ users{ logIn(username: $username, password: $password){ 세션토큰 } } }
Parse 3.8
변이 로그인유저($username: 문자열!, $password: 문자열!){ logIn(username: $username, password: $password){ sessionToken } }
Parse 3.9
변이 로그인유저($username: 문자열!, $password: 문자열!){ logIn(username: $username, password: $password){ 세션토큰 } }
마지막으로, 사용자가 로그아웃하는 변수는 매개변수가 필요하지 않고 부울만 반환합니다:
Parse 3.7.2
변이 로그아웃유저{ users{ logOut } }
Parse 3.8
변이 로그아웃 사용자{ logOut }
Parse 3.9
변이 로그아웃 사용자{ 로그아웃 }
이 모든 것을 UserMutations.graphql이라는 새 파일에 추가하고 이전 글에서 다운로드하는 방법을 배운 schema.graphql 파일도 저장합니다.
이 파일들은 다음에 추가할 것이므로 안전하게 보관하세요.
SwiftUI 프로젝트
SwiftUI 프로젝트를 만들어 봅시다.
XCode 11을 실행하고 XCode 시작하기 창에서 “새 XCode 프로젝트 만들기”를 누르세요.
해당 창이 표시되지 않으면 언제든지 파일 > 새로 만들기 > 프로젝트로 이동할 수 있습니다.
상단에서 iOS 앱을 선택한 다음 싱글 뷰 앱을 선택합니다. 좋은 이름을 지정하고 언어로는 Swift를, 사용자 인터페이스로는 SwiftUI를 선택하세요:
사용자 인터페이스로 SwiftUI를 선택하는 것을 잊지 마세요.
이제 웹 API 문서에서 배운 대로 Apollo 클라이언트를 설치한 후 UserMutations.graphql 및 schema.graphql 파일을 프로젝트에 추가합니다.
새로 생성된 API.swift 파일을 컴파일하여 프로젝트에 추가합니다.
이 시점에서 여러분이 기대하는 것은 다음과 같습니다:
- XCode 11 설치 및 실행
- XCode 프로젝트 내에 설치 및 통합된 Apollo 클라이언트
- XCode 프로젝트에 추가된 UserMutations.graphql 및 schema.graphql 파일
- XCode 프로젝트에 추가된 API.swift 생성 파일
프로젝트는 이와 비슷하게 보일 것입니다:
모든 것이 XCode 11에 통합되었습니다.
이제 SwiftUI를 시작할 준비가 되었습니다.
지금 Back4app에 가입하고 Instagram 클론 앱 빌드를 시작하세요.
컨트롤 결정하기
사용자를 로그인하거나 로그아웃하기 전에 먼저 해당 사용자를 생성해야 합니다. 따라서 먼저 가입 화면을 코딩하겠습니다.
일반적으로 이 화면에는 4~5개의 컨트롤을 사용할 수 있습니다:
- 사용자에게 이 화면이 어떤 화면인지 알려주는 텍스트 유형(텍스트)
- 사용자 이름(텍스트 입력 컨트롤)
- 비밀번호(보안 텍스트 입력 컨트롤)
- 비밀번호 확인(선택 사항, 플랫폼에 따라 다름, 보안 텍스트 입력 컨트롤)
- 이메일(형식화된 텍스트 입력 제어)
- 가입 버튼(버튼)
모바일 앱이므로 대부분의 모바일 플랫폼에서 사용자가 마지막으로 입력한 문자를 볼 수 있고 대부분의 경우 사용자가 비밀번호가 올바르게 입력되었는지 확인하기에 충분하므로 비밀번호 확인 텍스트 입력 컨트롤은 넣지 않기로 했습니다.
이러한 컨트롤은 위 순서대로 서로 아래에 세로로 정렬되어 표시되며 각각 고유한 색상으로 표시됩니다.
실제로 UI 구축하기
앞서 언급했듯이 저는 macOS Catalina를 사용하지 않기 때문에 미리 보기를 활성화하지 않지만, 제가 작성한 코드와 함께 결과를 컴파일하여 보여드리겠습니다.
파일 > 새로 만들기 > 파일로 이동한 다음 사용자 인터페이스 섹션에서 SwiftUI 보기를 선택하여 새 SwiftUI 보기를 만듭니다.
파일 이름을 SignUpView.swift로 지정하면 프로젝트에 통합되어 나타납니다.
새로 생성된 뷰에 이미 “Hello World!” 문자열이 포함된 텍스트 컨트롤이 있는 것을 볼 수 있습니다.
Swift의 장점은 해당 뷰에 필요한 컨트롤만 포함하는 간단한 뷰를 여러 개 만든 다음 여러 뷰를 병합하여 더 복잡한 뷰로 만들 수 있다는 것입니다. 이것이 바로 최고의 객체 지향입니다.
우리는 컨트롤을 세로로 정렬하기로 결정했고 SwiftUI에는 이를 위한 특정 컨트롤이 있습니다. VStack(수직 스택)이라는 이름의 컨트롤이 있습니다. VStack 안에 있는 모든 것은 자동으로 세로로 쌓이게 되므로 테스트를 위해 ContentView.swift에 VStack 컨트롤을 추가하고 그 안에 SignUpView를 두 번 추가해 보겠습니다:
VStack에 SignUpView를 두 번 추가했습니다: 세로로 두 번 정렬되어 표시됩니다.
이제 얼마나 멋진가요! 컨트롤이 더 필요하면 계속 추가하면 됩니다!
VStack과 중복성을 제거하고 해당 뷰에 SignUpView()를 하나만 만든 다음, 이미 테스트 결과를 확인했으므로 SignUpView로 돌아가서 변경을 시작하겠습니다. 이제 ContentView가 다음과 비슷하게 보일 것입니다:
구조체 ContentView: View { var body: 일부 View { SignUpView() } }
화면 상단에 텍스트가 있는 VStack이 필요하다는 것을 이미 알고 있으므로 SignUpView에 VStack을 추가하고 “Hello World!” 문자열을 “Sign Up”과 같은 유용한 것으로 변경해 보겠습니다. SignUpView는 다음과 같이 보일 것입니다:
구조체 SignUpView: View { var body: 일부 View { VStack{ Text("Sign Up") } } }
그 아래에 어떤 컨트롤을 추가해야 하는지 이미 알고 있으므로 추가할 수 있습니다:
- 사용자 이름에 대한 텍스트 필드
- 비밀번호를 위한 SecureField
- 이메일용 텍스트 필드
- 가입을 위한 버튼
텍스트 필드와 보안 필드는 모두 이전 UITextField와 매우 유사하지만 상태 바인딩에 의존해야 하므로 다음과 같이 선언하겠습니다:
Control(“Placeholder”, text: stateValueToBindTo)
로 선언하고 첫 번째 단계는 바인딩할 state를 선언하는 것입니다:
@State var username: String = "" State var password: String = "" State var email: String = ""
이제 컨트롤을 추가할 수 있습니다:
Text("Sign Up") TextField("사용자 이름", text: $username) SecureField("비밀번호", 텍스트: $password) TextField("이메일(선택 사항)", text: $email)
마지막으로 다음과 같은 구조의 Button을 추가할 수 있습니다:
Button(action: {
//트리거될 때 실행할 코드
}){
//Content
}
이미 눈치챘겠지만, UIButton과 비슷하게 작동하지만 프로그래밍 방식으로 콘텐츠를 정의할 수 있고 텍스트, 이미지 등 거의 모든 것을 추가할 수 있으므로 훨씬 더 유연한 구조를 가집니다. 무엇이든 추가할 수 있습니다.
기존의 대상/행동 구조는 사라지고 탭했을 때 동작을 처리하는 새로운 action:{} 구조로 대체되었습니다.
버튼에는 “가입하세요!”라는 텍스트가 포함될 것이므로 이를 추가해 보겠습니다:
버튼(action: { }){ Text("Sign Up!") }
최종 코드는 다음과 같아야 합니다:
구조체 SignUpView: View { @State var username: String = "" State var password: String = "" State var email: String = "" var body: 일부 뷰 { VStack{ Text("Sign Up") TextField("사용자 이름", text: $username) SecureField("비밀번호", text: $password) TextField("이메일(선택 사항)", text: $email) Button(action: { }){ Text("가입하세요!") } } } }
미리보기는 어떤가요?
유망해 보이지만 몇 가지 시각적 속성을 설정하여 많은 것을 개선할 수 있습니다.
SwiftUI의 멋진 점은 코드에 시각적 속성을 추가하고 실시간으로 변경 사항을 확인할 수 있다는 것입니다!
한번 해봅시다!
먼저 사용할 색상을 선언해 보겠습니다.
let lightGreyColor = Color(빨강: 239.0/255.0, 초록: 243.0/255.0, 파랑: 244.0/255.0, 불투명도: 1.0) let lightBlueColor = Color(빨강: 36.0/255.0, 녹색: 158.0/255.0, 파랑: 235.0/255.0, 불투명도: 1.0)
그런 다음 몇 가지 멋진 기능을 추가해 보겠습니다:
요소 사이에 약간의 패딩을 두어 공간을 확보합니다.
배경 및 전경색 속성을 설정하여 브랜드와 일관된 컨트롤을 갖도록 합니다.
모서리를 둥글게 만들 수 있도록 cornerRadius를 설정합니다.
그리고 글꼴과 글꼴 무게를 설정하여 더 멋지게 만듭니다.
짜잔!
멋지지 않나요?
이 시점에서 코드를 추적해 보세요:
구조체 SignUpView: View { 상태 변수 사용자 이름: 문자열 = "" State var password: String = "" State var email: String = "" let lightGreyColor = Color(빨강: 239.0/255.0, 초록: 243.0/255.0, 파랑: 244.0/255.0, 불투명도: 1.0) let lightBlueColor = Color(빨강: 36.0/255.0, 녹색: 158.0/255.0, 파랑: 235.0/255.0, 불투명도: 1.0) var body: 일부 뷰 { VStack{ Text("Sign Up") .font(.largeTitle) .foregroundColor(lightBlueColor) .fontWeight(.semibold) .padding(.bottom, 20) 텍스트필드("사용자명", 텍스트: $username) .padding() .background(lightGreyColor) .cornerRadius(5.0) .padding(.bottom, 20) 보안 필드("비밀번호", 텍스트: $password) .padding() .background(lightGreyColor) .cornerRadius(5.0) .padding(.bottom, 20) TextField("이메일(선택 사항)", text: $email) .padding() .background(lightGreyColor) .cornerRadius(5.0) .padding(.bottom, 20) Button(action: { }){ 텍스트("가입하세요!") .font(.headline) .foregroundColor(.white) .padding() .frame(너비: 220, 높이: 60) .background(lightBlueColor) .cornerRadius(15.0) } }.padding() } }
이제 멋지고 반짝이는 화면이 거의 완성되었습니다.
어때요…
일부 기능
이제 UI가 준비되었으므로 이제 GraphQL 메서드에 Apollo 호출을 추가할 차례입니다.
거의 모든 뷰에서 이러한 호출을 사용할 것이므로 모든 뷰에서 액세스할 수 있는 위치에 Apollo 클라이언트를 만들어야 합니다. 그 위치가 AppDelegate.swift입니다.
그 아래에 Apollo 클라이언트를 선언합니다.
import UIKit
와 코드 블록의 시작 부분
@UIApplicationMain 앱디렉티브 클래스를 생성합니다: UIResponder, UIApplicationDelegate { ...
이전 글에서 보여드린 것처럼 이제부터 모든 뷰에서 GraphQL 쿼리 및 변형을 수행할 수 있습니다.
먼저 Apollo 프레임워크를 임포트한 다음 다음과 같이 클라이언트를 인스턴스화해야 합니다:
import Apollo // Apollo 클라이언트 초기화. // 자세한 내용은 여기: https://www.back4app.com/docs/ios/swift-graphql let apollo: ApolloClient = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ "X-Parse-애플리케이션-아이디": "YourAppIdHere", "X-Parse-Client-Key": "YourClientKeyHere" ] let url = URL(문자열: "https://parseapi.back4app.com/graphql")! 반환 ApolloClient( networkTransport: HTTPNetworkTransport( url: url, 구성: 구성 ) ) }()
YourAppIdHere 및 YourClientKeyHere 문자열을 현재 값으로 바꾸기만 하면 계속 진행할 수 있습니다.
짐작하셨겠지만, 가입 버튼을 클릭할 때 SignUp에 대한 변형을 수행해야 하므로 액션 블록에서 이를 호출해 보겠습니다:
// 방금 텍스트 필드에서 가져온 매개 변수를 전달하여 SignUpUser 변이를 수행합니다. apollo.perform(mutation: SignUpUserMutation(username: self.username, password: self.password, email: self.email)){ result in // 성공적인 결과와 오류를 구분할 수 있도록 결과를 전환해 보겠습니다. switch result { // 성공의 경우 case .success(let graphQLResult): // 결과를 Parse해 봅니다. if let objId = graphQLResult.data?.users?.signUp.objectId { { print ("오브젝트아이디로 생성된 사용자: " + 오브젝트아이디) } // 하지만 그래프QL 오류가 발생하면 해당 메시지를 표시합니다. else if let errors = graphQLResult.errors { // 그래프QL 에러 print(errors) } // 실패한 경우 해당 메시지를 표시합니다. case .failure(let error): // 네트워크 또는 응답 형식 오류 print(error) } }
이 방법은 작동하지만 print() 메서드는 콘솔에만 인쇄됩니다. 사용자에게 알림을 표시해야 하므로 다음과 같은 구조의 알림으로 변경해 보겠습니다:
Alert(title: Text(“Title”), message: Text(“Message”), dismissButton: .default(Text(“TextOfButton”)))
런타임에 제목과 메시지를 변경해야 하므로, 자체 변수는 런타임에 변경할 수 없으므로 해당 값을 처리할 구조체를 설정해야 합니다:
구조체 Message { var alertTitle: String = "" var alertText: String = "" } var myMessage = Message()
또한 뷰가 언제 경고를 표시할지 알 수 있도록 State를 만듭니다:
State private var showingAlert = false
버튼 액션의 전체 코드는 다음과 같아야 합니다:
Button(action: { // 방금 텍스트 필드에서 가져온 매개변수를 전달하여 SignUpUser 변형을 수행합니다. apollo.perform(mutation: SignUpUserMutation(username: self.username, password: self.password, email: self.email)){ result in // 성공적인 결과와 오류를 구분할 수 있도록 결과를 전환해 보겠습니다. switch result { // 성공의 경우 case .success(let graphQLResult): // 결과를 Parse해 봅니다. if let objId = graphQLResult.data?.users?.signUp.objectId { { myMessage.alertTitle = "Yay!" myMessage.alertText = "사용자가 가입했습니다!" self.showingAlert = true 인쇄 ("ObjectId로 생성된 사용자: " + objId) } // 하지만 그래프QL 오류가 발생하면 해당 메시지를 표시합니다. else if let errors = graphQLResult.errors { // 그래프QL 오류 myMessage.alertTitle = "죄송합니다!" myMessage.alertText = "GraphQL 오류가 발생했습니다: " + errors.description self.showingAlert = true print(errors) } // 실패한 경우 해당 메시지를 표시합니다. case .failure(let error): // 네트워크 또는 응답 형식 오류 myMessage.alertTitle = "죄송합니다!" myMessage.alertText = "오류가 발생했습니다: " + error.localizedDescription self.showingAlert = true print(error) } } }){ Text("가입하세요!") .font(.headline) .foregroundColor(.white) .padding() .frame(너비: 220, 높이: 60) .background(lightBlueColor) .cornerRadius(15.0) } .alert(isPresented: $showingAlert) { Alert(title: Text(myMessage.alertTitle), message: Text(myMessage.alertText), dismissButton: .default(Text("OK"))) }
유망해 보이죠?
그럼 이제 곧
컴파일하고 실행합니다. 새 사용자를 등록하면 다음과 같은 메시지가 표시됩니다.
Parse 대시보드에서 사용자가 생성되었는지 확인해 봅시다!
결론
Apollo 클라이언트를 사용하여 GraphQL 변형을 수행하는 완전히 작동하는 SwiftUI 뷰를 만들었습니다. 얼마나 멋진가요!
Instagram 클론 앱에 대한 작업을 계속 진행하겠습니다! 다음 단계는 로그인과 로그아웃이며 이미 게시물이 만들어졌습니다!
계속 지켜봐 주세요!
참조
- 이 시리즈의 1부는 Instagram 클론입니다.
- 2부는 Instagram 로그인입니다.
- 3부는 Instagram 프로필입니다.
- 4부는 Instagram 홈뷰입니다.
- 앱 백엔드는 iOS Instagram 클론 백엔드에서 확인하세요.
- 소스 코드가 포함된 iOS Instagram 클론 프로젝트를 다운로드하고 Back4App 사용을 시작하세요.
지금 Back4app에 가입하고 Instagram 클론 앱 빌드를 시작하세요.