Drag and Drop은 iOS 11에 도입 된 기술로, Application
을 사용하는 사람이 Application
내에서 또는 Application
간에 데이터를 전송할 수있는 강력한 방법을 제공합니다.
UIDragInteraction
UIDropInteraction
접근성 요소가 직접 Drag-Drop을 호스팅하지 않을 수 있습니다.
UIAccessibilityDragging
: drag sources / drop points 정의하는 protocol
extension NSObject {
@available(iOS 11.0, *)
open var accessibilityDragSourceDescriptors: [UIAccessibilityLocationDescriptor]?
@available(iOS 11.0, *)
open var accessibilityDropPointDescriptors: [UIAccessibilityLocationDescriptor]?
}
UIAccessibilityDragging
레이어는 뷰가 아니기 때문에 상호작용을 호스팅 할 수 없습니다. > 막대 그래프 뷰에 넣어야 합니다.
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
if let index = self.indexOfBar(point: session.location(in: self)) {
let provider = NSItemProvider(object: "Bar: \(series[index])" as NSString)
let dragItem = UIDragItem(itemProvider: provider)
dragItem.localObject = index
return [dragItem]
}
return []
}
func makeAccessibilityElements() {
self.accessibilityElements = bars.enumerated().map { (index, barLayer) in
let element = UIAccessibilityElement(accessibilityContainer: self)
element.accessibilityFrameInContainerSpace = barLayer.frame
element.accessibilityLabel = seriesLabels[index]
element.accessibilityValue = "\(series[index])"
return element
}
}
UIAccessibilityLocationDescriptor
accessibilityDragSourceDescriptors
: 논리적으로 이 요소와 연관된 drag source
를 노출accessibilityDropPointDescriptors
: 논리적으로 이 요소와 연관된 drop point
를 노출Descriptor
는 실제 view를 참조하고 있습니다.let descriptor = UIAccessibilityLocationDescriptor(name: “Drag from specified point",
point: dragPoint, in: view)
element.accessibilityDragSourceDescriptors = [descriptor]
func makeAccessibilityElements() {
self.accessibilityElements = bars.enumerated().map { (index, barLayer) in
let element = UIAccessibilityElement(accessibilityContainer: self)
element.accessibilityFrameInContainerSpace = barLayer.frame
element.accessibilityLabel = seriesLabels[index]
element.accessibilityValue = "\(series[index])"
// 뷰의 dragPoint를 사용해서 descriptor를 생성하여 element에 넣어준다.
let dragPoint = CGPoint(x: barLayer.frame.midX, y: barLayer.frame.midY)
let descriptor = UIAccessibilityLocationDescriptor(name: "Drag bar data", point:dragPoint, in: self)
element.accessibilityDragSourceDescriptors = [descriptor]
return element
}
}
accessibilityDropPointDescriptors
를 사용해서 노출시킬 드랍포인트를 지정해주고 UIAccessibilityLocationDescriptor
로 드래그할 대상을 지정해줍시다!
override var accessibilityDropPointDescriptors: [UIAccessibilityLocationDescriptor]? {
get {
let photoWellMidpoint = CGPoint(x: self.contactPhotoWell.bounds.midX,
y: self.contactPhotoWell.bounds.midY)
let attachmentsWellMidpoint = CGPoint(x: self.attachmentsWell.bounds.midX,
y: self.attachmentsWell.bounds.midY)
return [
UIAccessibilityLocationDescriptor(name: "Drop into portrait", point: photoWellMidpoint, in: self.contactPhotoWell),
UIAccessibilityLocationDescriptor(name: "Drop into attachments", point: attachmentsWellMidpoint, in:self.attachmentsWell)]
}
set {}
}
AVKit
은 CoreMedia
의 AVFoundation
을 기반으로 구축 된 크로스 플랫폼 미디어 재생 UI framework입니다. Apple의 자체 앱에서 사용하는 것과 동일한 사용자 인터페이스를 사용하여 AVPlayer
기반 미디어 컨텐츠를 쉽게 표시하고 재생할 수 있도록하는 것이 우리의 임무입니다.
AVPlayerViewController
를 제공합니다.APKit
앱의 경우 AVPlayerView
를 제공합니다.AVRoutePickerView
를 제공합니다.// Media Playback with AVPlayerViewController on iOS and tvOS
import AVKit
// 1) AVPlayer 만들기
let player = AVPlayer(url: “https://my.example/video.m3u8”)
// 2) AVPlayerViewController 만들고 AVPlayer 객체 할당해주기
let playerViewController = AVPlayerViewController()
playerViewController.player = player
// 3) present AVPlayerViewController
present(playerViewController, animated: true)
이렇게 하면 Apple이 제공하는 가장 기본적인(그리고 기본 앱과 동일한) UI의 미디어플레이어를 생성할 수 있습니다. 또한 CoreMedia
플레이어 UI안에서 AVFoundation
의 모든 기능 - AVPlayer
, AVPlayerItem
, AVAsset
- 들을 사용할 수 있습니다.
그러면서도 AVKit은 UIKit 및 AppKit 위에 있기 때문에 각 Apple platform에 대응되는 고유 UI를 가지기 때문에 더 나은 경험을 제공할 수 있습니다.
AVPlayerViewControllerDelegate
: 전체 화면 재생이 시작되거나 끝날 때 알림을 받을 수 있음, iOS 12 부터 가능
@available(iOS 12.0, *)
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator)
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { (context) in
// Add coordinated animations
}) { (context) in
// 여기서 전환이 성공햇는지, 취소됐는지 알 수 있습니다.
if context.isCancelled {
// Still embedded inline
} else {
// Presented full screen
// Take strong reference to playerViewController if needed
}
}
}
만약 스크롤뷰에 플레이어 뷰 컨트롤러가 있다고 했을 때 전체화면 상태에서 기기가 회전하면서 스크롤뷰 오프셋이 변경되는 경우가 있는데, 테이블 뷰나 컬렉션 뷰에서 이럴 경유 뷰 자체가 할당해제될 수 있어서 문제가 될 수 있습니다. 그래서 context가 정상적으로 전체화면으로 변경되었는지 확인한 다음 필요하다면 강한 참조
를 사용해야 합니다.
Title, artwork, 혹은 그 외의 메타데이터를 AVKit이 자동으로 처리합니다.
혹은 원하는 Metadata를 보완하도록 할 수 있습니다.
@available(iOS 12.0, *)
extension AVPlayerItem {
open var externalMetadata: [AVMetadataItem] }
}
showsPlaybackControls = false
AVPlayerViewController
modallycontentOverlayView
UIWindowScene
coordinate space
// Add as child:
parent.addChild(playerViewController)
parent.view.addSubview(playerViewController.view) // Or other UIView insertion API
// Enable auto layout and set up constraints
playerViewController.didMove(toParent: parent)
// Remove from parent:
playerViewController.willMove(toParent: nil)
playerViewController.view.removeFromSuperview()
playerViewController.removeFromParent()
// 재생 컨트롤을 표시하지 않음:
playerViewController.showsPlaybackControls = false
// 전체 화면을 채우도록 함:
playerViewController.videoGravity = .resizeAspectFill
// 필요하다면 백그라운드 컬러 변경:
playerViewController.view.backgroundColor = .clear // Or any other UIColor
AVPlayer.allowsExternalPlayback
비활성화AVAudioSession
을 2차 media playback으로 설정
.ambient
categoryAVAudioSession.silenceSecondaryAudioHintNotification
AVAudioSession.secondaryAudioShouldBeSilencedHint
// Media Playback with AVPlayerViewController on iOS and tvOS
import AVKit
// 1a) Create an AVPlayer
let player = AVPlayer(url: “https://my.example/video.m3u8”)
// 1b) Add external metadata if needed
player.currentItem?.externalMetadata = // Array of [AVMetadataItem]
// 2) Create an AVPlayerViewController
let playerViewController = AVPlayerViewController()
playerViewController.player = player
// 3) Show it
present(playerViewController, animated: true)
AVPlayerViewControllerDelegate
를 사용합시다.
AVPlayerItem
를 미리 설정해두기
AVPlayer.status
와 AVPlayerItem.status
관찰해서 제어하기
.readyToPlay
error
property when status is .failed
.mediaServicesWereReset
AVPlayer.usesExternalPlaybackWhileExternalScreenIsActive
AVAudioSession
for .playback
AVPlayerViewControllerDelegate
사용해서 트래킹AVPlayerViewController
when full screenmodalPresentationStyle
as .fullScreen
videoGravity
view.layer.cornerRadius, cornerCurve, and maskedCorners
view.backgroundColor
entersFullScreenWhenPlaybackBegins
exitsFullScreenWhenPlaybackEnds
URL
이 있다면 LinkPresentation
Framework를 사용해서 LP metadata provider
클래스를 사용하여 메타데이너를 쉽게 가져올 수 있다. 그러면 LPLinkMetada
가 반환되는데, 여기에는 기본적으로 title이 포함되어 있다.
// URL로부터 link presentation에 사용될 metadata 가져오기
let metadataProvider = LPMetadataProvider()
let url = URL(string: "https://www.apple.com/ipad")!
metadataProvider.startFetchingMetadata(for: url) { metadata, error in
if error != nil {
// 네트워크가 너무 느리거나, 없을 경우 오류가 나타난다.
return
}
// TODO: Make use of fetched metadata.
}
LPLinkMetada
is serializablelocal file URLs
QuickLook
썸네일 API를 사용해서 파일의 대표 썸네일을 가져올 수 있다.// URL로부터 link presentation에 사용될 metadata 가져오기
let metadataProvider = LPMetadataProvider()
let url = URL(string: "https://www.apple.com/ipad")!
metadataProvider.startFetchingMetadata(for: url) { metadata, error in
if error != nil {
// 네트워크가 너무 느리거나, 없을 경우 오류가 나타난다.
return
}
// TODO: Make use of fetched metadata.
let linkView = LPLinkView(metadata: metadata)
cell.contentView.addSubview(linkView)
linkView.sizeToFit()
}
LPLinkView
는 intrinsic size를 가지지만 주어진 제약에 맞게 size를 조절할 수 있다.
타이틀과 이미지가 뜨는 데 시간이 다를 수 있다. 서버를 찔러야 하기 때문이징!
하지만 한번 LPLinkMetadata
를 가져오고 나면 빠르게 불러올 수 있다.
// UIActivityViewController에 UIActivityItemSource를 이용해서 prefetch된 metadata 전달
func activityViewControllerLinkMetadata(_: UIActivityViewController) -> LPLinkMetadata {
return self.metadata
}
만약 metadata를 이미 가지고 있다면 굳이 서버로 통신할 필요 없이 metadata 객체를 만들어서 반환하면 된다.
// 커스텀 metadata 전달
func activityViewControllerLinkMetadata(_: UIActivityViewController) -> LPLinkMetadata {
let metadata = LPLinkMetadata()
metadata.originalURL = URL(string: "https://www.example.com/apple-pie")
metadata.url = metadata.originalURL
metadata.title = "The Greatest Apple Pie In The World"
metadata.imageProvider = NSItemProvider.init(contentsOf: Bundle.main.url(forResource: "apple-pie", withExtension: "jpg"))
return metadata
}
LPMetadataProvider
to fetch rich metadata for a URLLPLinkView
to beautifully present links in your appLPLinkMetadata
to accelerate the share sheet preview in your appiOS 13에서부터 멀티 윈도우를 지원합니다. 기존 앱을 더욱 유용하게 만들고 사용자의 생산성을 크게 향상시키는 환상적인 방법입니다. 이 세션은 다음 세 가지를 다룹니다.
UISceneDelegate
를 알아보고 어떤 작업을 해야하는지Architecture Kit
의 모범사례iOS 12 및 이전 버전에서는 하나의 Application 프로세스와 하나의 UI 인스턴스만 존재했기 때문에 관계없지만, 그 이후에는 여러 화면이 존재할 수 있으므로 App Delegate
의 역할과 책임이 바뀌어야 합니다.
하지만 iOS 12 및 이전 버전을 유지하기 위해서는 AppDelegate 로직을 유지해야 합니다..!
App Delegate
didFinishLaunchingWithOptions(:)
scene session
UIScene
UISceneDelegate
, Storyboard
, Subclass View
Scene Delegate
willConeectToSession(:)
willResignActive
, didEnterBackground
didDisconnected
App Delegate
didDiscardSceneSession
이 호출된다.프로세스가 실행중이 아닌 경우 시스템은 discard된 세션을 추적하고 애플리케이션이 실행된 직후 이를 다시 호출합니다.
앱의 화면이 여러 개가 사용될 수 있기 때문에 화면에 대한 복구가 필요합니다. 만일 상태 복원을 구현하지 않으면 멀티 윈도우 상태에서 disconnect 된 후 다시 연결되었을 때 완전히 새로운 화면이 뜰 것이기 때문에 사용자 경험에 좋지 않을 것입니다.
하지만 iOS 13에서는 이를 지원하기 위해 State Restoration API를 제공합니다. 상태를 기반으로 화면을 인코딩할 수 있으며 이는 NSUserActivity
를 기반으로 합니다.
spotlightsearch
나 handoff
에도 적용할 수 있습니다.만약 채팅 화면을 여러 곳에서 띄운다면? 같은 채팅방을 여러 창에서 띄울 때 동기화 문제를 어떻게 해야할까요?
ViewController
가 이벤트를 수신하면 Model Controller
에 다시 이벤트를 전달Model
을 구독하고 있는 구독자 혹은 View Controller
에게 새로운 데이터가 업데이트 되었음을 알림실제 구현 방식으로는 Delegate
와 Notification
, Swift Combine
을 사용할 수 있습니다.
우리는 AppDelegate
와 SceneDelegate
의 차이점과 책임의 차이에 대해 살펴 보았습니다. 우리는 또한 몇 가지 주요 SceneDelegate
method
를 알아보고 여기에서 어떤 종류의 작업을 해야하는지 살펴 보았습니다. 또한 iOS 13에서 상태 복원이 중요한 이유와 새로운 장면 기반 API를 활용하는 방법에 대해서도 설명했습니다. 마지막으로, 단방향 데이터 흐름을 생성하기 위한 일부 고수준 패턴에 대해 이야기하며, 모든 장면이 동일한 데이터를 공유하는 동안 동기화 상태를 유지할 수 있도록 하였습니다.
기존에는 UIColor를 사용할 때 RGB value 를 썼지만 다크모드가 나오면서 이에 대응할 수 있는 다양한 기본 Color들이 제공된다. Table systempGroupedBackground, systemBlue 등등~ 기본 UIComponent들은 기본적으로 다크모드를 대응되니까 필요한 부분만 customizing 해서 사용하자. 배경이나 테이블 뷰 배경 같은 이런 기본적인 애들은 걍 있는 컬러 가져다 쓰면 너도 좋고 나도 좋고 코드도 좋다~~
Modal 창을 보여주는 방식이 변화하면서 + 다크모드가 등장하면서 다른 화면 위에 뜨는 화면이 늘어남 그래서 그 경우 elevated level이라고 하고 system background 컬러로 지정하면 level에 따라 layer가 추가되어 다른 색상이 나온다.
Dark Mode인지 Light Mode인지는 TraitCollection
안에 user userInterfaceStyle
로 들어가 있다.
그리고 각 view
와 view controller
는 TraitCollection
을 가진다.
Custom dynamic color를 사용해야 한다면 traitCollection을 가지고 color를 결정할 것. traitCollection은 current value를 가지고 있기 때문에 유용하지만 위험할 수 있다.
traitCollection은 다음 메서드들에서 사용된다.
이 외에서는 TraitCollection
이 올바른 값을 가지고 있을지, 기존 값을 가지고 있을지 보증하지 않는다.
Core 단, CoreAnimation Framework는 이 Dynamic Color/Trait의 개념을 가지고 있지 않기 때문에, CALayer를 사용하는 경우는 조금 다르게 다루어야 한다. (cgcolor는 dynamic 하지 않음)
let newTraitCollection = traitCollection
UIColor.resolveColor(with: newTraitCollection)
newTraitCollection.performAsCurrent { }
view.traitCollection = newTraitCollection
Size class가 변경되었을 때도 traitCollectionDidChanged가 불리니까 traitCollection color appearance did changed 메서드를 활용해서 컬러가 변경되었을 때에만 적절한 조취를 취하자.
UIImageView는 TraitCollection과 상관 없다. image만 잘 가져오면 된다. image asset 에서 가져오면 된다.
UIImage(named: “imageName”)?.imageAsset?.(with: newTraitCollection)
TraitCollection
은 앱 전체에서 하나만 존재하는 것이 아니라, ViewHierarch
의 최상단인 UIScreen
부터 가지고 있다. UIWindowScene, UIWindow, UIPresentationController, UIViewController, UIView …
그렇기 때문에 현재 보이는 화면의 traitcollection을 사용하는 것이 적절하다~
뷰를 추가해본다고 생각해보자. 가장 가까운 목적지(보이는 화면?)의 traitCollection을 가져와서 view에 미리 할당해준다. 그리고 실제로 addSubView가 되면 그때 부모 에게서 traitcollection을 다시 가져온다.
특성이 변경될 때에만 traitcollection이 변경되었다는 메소드가 불린다.
디버그 로깅에 trait collection 로깅+뭐가 바꼈는지 알 수 있도록 추가할 수 있지~
trait은 레이아웃이 일어나기 전에 항상 변경된다.
만약 trait collection을 일부 화면에서 혹은 뷰에서 고정하고 싶다면 overrideUserInterfaceStyle
을 override하자
앱 전체에서 설정할거면 plist에서 UserInterfaceStyle을 설정해주자.
traitCollection 자체를 override할 수도 있는데, trait collection에는 style만 있는게 아니니까 super를 꼭 불러 준 다음에 필요한 항목을 지정해주자.
Status Bar도 UserInterface Style에 따라 바뀐다.
UILAbel, UITextField, UITextView 는 기본적으로 label color를 사용한다.
NSAttributedString.Key: Any 에서는 foregroundColor에 그냥 Dynamic color 쓰면 먹힘
webcontent에서는 color-scheme prefers-color-scheme 등의 태그를 사용해서 다크모드에 대응할 수 있다.
필요하다면 다른 세션 있으니까 거기서 찾아보자.
안녕하세요! caution입니다.
지난 포스팅에서는 Django 프로젝트를 위한 설정 및 DjangoGirls 프로젝트를 시작해보았습니다. 블로그 앱을 만들기 위해서 Post
model 클래스도 만들었고, admin 페이지에 포스팅을 관리할 수 있도록 등록하는 과정까지 진행해보았습니다!
그리고 오늘의 목표는 다음과 같습니다 :)
post_list
페이지에 발행날짜(published_date
)가 최신인 포스팅들을 보여주기목표가 정해졌으니, 차근차근 달려볼까요~
참고로, 이 포스팅 DjangoGirls 튜토리얼 컨텐츠를 기반으로 작성되었습니다. 해당 포스팅은 다음 컨텐츠들을 포함합니다. 장고 ORM과 쿼리셋(QuerySets / 템플릿 동적 데이터 / 장고 템플릿
지난 시간에 만들었던 관리자 페이지에서 내가 만들었던 Post 목록을 볼 수 있었습니다. 기억나시죠? 여기에 보이는 데이터들은 모든 포스팅들을 보여주고 있어요.
물론 모든 포스팅을 보는 목록형 페이지도 필요하겠지만, 블로그 앱을 만들려면 더 많은 기능들이 필요할 수 있어요. 예를 들어,
이런 기능은 당연히 있어야겠죠? 이런 기능들이 없다면 원하는 포스팅을 찾으려고 할 때 너무 어려울 것 같아요 ㅠ.ㅠ 하지만 또 이걸 일일이 구현하기에는.. 어려울 수도 있다고 생각해요..! 매번 반복문을 돌면서 조건을 걸어서 필터링 할 수도 없고!
그래서 Django
에서는 자체적으로 Model
객체들을 여러 조건을 주어 검색할 수 있도록 도와주는 QuerySet 클래스가 있습니다.
이전 시간에 DB Browser for SQLite
를 통해서 Database에 접근했던 걸 기억하시나요? Database의 데이터를 조회하고, 수정하고, 새로운 데이터를 추가할 수 있는데요, 웹 서버에서는 Database GUI Tool을 사용할 수 없기 때문에 직접 프로그래밍 코드로 데이터를 관리할 수 있도록 SQL(Structured Query Language)
을 사용합니다.
예를 들어, 모든 포스팅 데이터를 가져오려면?
select * from Post
간단히 설명하자면,
Post
테이블에서 모든 데이터를 출력하는데, 모든 attributes(*)를 출력해 달라는 의미에요.
당황하지 마세요. 저희는 지금 SQL을 배우려는 게 아닙니다. 물론 알고 있다면 더 이해하기 쉽겠지만(또 언젠가는 알아야하지만), Django
에서는 개발자가 SQL구문을 직접 사용하지 않아도 되도록 QuerySet
을 만들었어요.
그래서 QuerySet
이 뭐냐?
QuerySet은 SQL Query 구문과 그 구문에 대한 데이터를 가지는 class입니다.
아까 모든 포스팅 데이터를 가져오는 SQL query를 작성해보았는데요, 이걸 QuerySet
형태로 변경해봅시다.
QuerySet
을 사용해보기 전에, Django Shell
에 들어가볼 거에요.
이전에 python
가상환경 안에 들어갔던 것을 기억하나요? 마찬가지로 Django Shell
에서는 python
명령어를 포함해서 Django
가 지원하는 여러가지 마법같은 명령어들을 사용할 수 있답니다.
$ python manage.py shell
그럼 다음과 안내문구가 나타나면서 Django Shell 안쪽으로 들어갈 수 있습니다 :)
저는 좀 더 향상된 기능을 가진 셸을 사용하기 위해 IPython을 설치해서 사용중입니다
pip install ipython
명령으로 IPython을 설치하면, manage.py shell 명령을 실행했을 때, 기본 셸 대신 IPython셸로 실행되어 좀 더 편하게 많은 기능들을 사용할 수 있습니다!
이 Django Shell
에서는 현재 위치의 Django Project
에 정의된 Class들에 접근이 가능합니다! 그럼 이제 어떤 포스팅들이 있는지 바로바로 가져와볼게요 :)
Post.objects.all()
오잉, 그럼 바로 오류가 나타나요..! Post 가 정의되지 않았다고 하는데요?
명령어를 입력하는 화면이 저랑 다르더라도 걱정마세요! 원래는
>>>
요렇게 시작해야하는 게 맞는데 저는IPython
을 설치해서 사용하고 있기 때문에 다른 화면으로 보이는 거랍니다. XD
이전 시간에 파일에서 다른 파일에 정의된 class
나 method
를 가져와야 할 때 어떤 명령어를 맨 위에 써주었습니다! 바로 import
인데요, 마찬가지로 Django Shell
에서도 blog.models
에 정의되어 있는 Post
class
를 import
해줄게요.
from blog.models import Post
자 그리고 나서 다시 위의 구문을 입력해 봅시다.
In [2]: Post.objects.all()
Out[2]: <QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
오, 우리가 작성했던 포스팅이 보이는군요! 포스팅들이 배열[]
형태로 들어가 있고, 이 포스팅들을 QuerySet
이라는 클래스 객체가 가지고 있어요.
QuerySet
을 조금 더 살펴봅시다. QuerySet은 아까 제가 "SQL Query 구문과 그 구문에 대한 데이터를 가지는 class입니다."
라고 적어놨어요. 위의 결과물을 보면 데이터를 가지고 있는 것은 확인됐는데, SQL Query
구문은 어디있을까요?
새로운 QuerySet
객체를 만들어봅시다.
In [3]: all_posts = Post.objects.all()
이번에는 아마 포스팅 리스트들이 나타나지 않았을 거에요. 그 상태에서 다음 구문들을 작성해봅시다.
In [4]: all_posts.query
Out[4]: <django.db.models.sql.query.Query at 0x10dc53550>
In [5]: print(all_posts.query)
SELECT "blog_post"."id", "blog_post"."author_id", "blog_post"."title", "blog_post"."text", "blog_post"."created_date", "blog_post"."published_date" FROM "blog_post"
QuerySet
class는 query
라는 attribute
를 가집니다. 이 query
가 실제 Database에서 데이터를 불러올 때 사용되는 SQL Query
입니다. 형태가 조금 다르지만, 위에서 작성했던 SQL문과 유사한 형태를 가지고 있죠? SELECT
다음에 어떤 attribute
들을 출력할 건지 정의하고, FROM
구문으로 어떤 테이블에서 데이터를 가져올 건지 명시해주었습니다.
사실 이 QuerySet
은 생성될 때마다 Database에서 데이터를 가져오지 않습니다. 우리가 Post.objects.all()
을 입력했을 때 QuerySet
인스턴스가 생성되고, 이 때에는 사실 query
만을 가지고 있고 데이터를 가지고 있지 않습니다.
이걸 검증하고 싶은데 어떻게 검증할 수 있을지 지금 딱 방법이 생각나지 않네요.
하지만 print(all_posts)
를 입력하면?
In [6]: print(all_posts)
<QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
우리에게 데이터를 보여주기 위해서 직접 Database에서 query
를 수행하여 데이터를 가져옵니다! 매번 데이터를 가져오는 것이 아니라 꼭 필요한 시점에 Database에 접근하기 때문에 좀 더 효율적이겠죠?
그렇다면 QuerySet은 언제 Database에 접근할까요? 이건 Django 공식홈페이지 (When querysets are evaluated?)에 잘 나와 있답니다. 한 번 정독해보시는 걸 추천..! 아직 저도 안해봤지만요 헤헤헤 다음에 정리해볼게요!
대략 정리하자면, QuerySet
객체는 내부에 데이터를 가져올 수 있는 query
문을 가지고 있고, 이 query
문을 사용하여 데이터베이스의 데이터를 가져옵니다. 한 번 데이터를 가져오고 나면 (데이터가 변경되는 등 새로고침되어야 하는 조건이 발생하지 않았다고 하면) 다시 요청이 들어왔을 때 매번 데이터를 가져오는 것이 아니라 이미 가져온 데이터(a.k.a cached data)를 넘겨줍니다.
QuerySet
에 대해 간략히 알아봤습니다. 이제 본격적으로 QuerySet
를 가지고 놀아 봅시다.
사실 이 기능은 위에서 살짝 해보았습니다. 다시 확인해볼까요?
# 모든 Post 객체들을 가져오기
In [7]: all_posts = Post.objects.all()
하지만 내가 원하는 조건을 가진 Post
만 가져오려면 어떻게 해야할까요? 예를 들어, 글쓴이로 검색을 한다고 해보자구요. 내가 작성한 글만 조회하려면?
In [8]: my_post = Post.objects.get(author=me)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-16-eb41f8b5902c> in <module>
----> 1 my_post = Post.objects.get(author=me)
NameError: name 'me' is not defined
오 오류가 나타났네요. me라는 이름을 가진 객체는 정의되지 않았대요. 이 오류를 잡기 전에 문법을 잠깐 볼까요?
저는 my_post
라는 변수에 Post
객체들 중 author
attribute
가 me
에 대응하는 데이터를 가져오도록 명령어를 주었어요. 지난 시간에 Post
를 정의할 때 author
외에 다른 attribute
들이 있었잖아요? title
, text
, created_date
, published_date
모두 다음과 같은 문법으로 사용할 수 있어요.
Model.objects.get('조건 대상 attribute 명'='조건')
자 다시 돌아와서, author=me
라는 조건을 주려면 현재 사용자 객체(me)를 정의해야 해요. 그러려면 아까 Post
class
를 사용하기 전처럼 User
class
를 import
해줍니다.
In [9]: from django.contrib.auth.models import User
지난 번에 우리가 manage.py
의 명령어로 관리자 사용자를 만들었던 걸 기억하시나요? 사용자는 여러명이 있을 수도 있으니, 현존하는 모든 사용자들을 확인해봅시다.
In [10]: User.objects.all()
Out[10]: <QuerySet [<User: caution>]>
오, 일단 사용자는 하나뿐이군요. 그럼 이 사용자를 me
에 넣어주면, author=me
를 사용할 수 있겠어요.
In [11]: me = User.objects.get(username='caution')
In [12]: my_post = Post.objects.get(author=me)
---------------------------------------------------------------------------
MultipleObjectsReturned Traceback (most recent call last)
<ipython-input-20-eb41f8b5902c> in <module>
----> 1 my_post = Post.objects.get(author=me)
~/.pyenv/versions/3.6.8/envs/djangogirls-env/lib/python3.6/site-packages/django/db/models/manager.py in manager_method(self, *args, **kwargs)
80 def create_method(name, method):
81 def manager_method(self, *args, **kwargs):
---> 82 return getattr(self.get_queryset(), name)(*args, **kwargs)
83 manager_method.__name__ = method.__name__
84 manager_method.__doc__ = method.__doc__
~/.pyenv/versions/3.6.8/envs/djangogirls-env/lib/python3.6/site-packages/django/db/models/query.py in get(self, *args, **kwargs)
410 raise self.model.MultipleObjectsReturned(
411 "get() returned more than one %s -- it returned %s!" %
--> 412 (self.model._meta.object_name, num)
413 )
414
MultipleObjectsReturned: get() returned more than one Post -- it returned 2!
오오오오오오류다! 오류가 나타났다! 여러분 진정합시다. 오류는 나쁘지 않아요.
오류 문구를 잘 살펴보면 어떤 부분을 수정해야 할 지 알 수 있습니다. 다음 단계에서 왜 이 오류가 나타났는지 살펴볼게요.
오류의 이름이 뭔가요? MultipleObjectsReturned
입니다. 여러 객체가 반환되었다는 거에요. get
명령어의 반환값은 단일 객체여야 합니다. 제가 작성한 포스팅이 여러 개이기 때문에 이런 오류가 나타난 거에요. 이럴 경우에는 filter를 사용해줍니다. 같은 명령을 get
대신 filter
로 바꿔볼게요.
In [13]: my_post = Post.objects.filter(author=me)
In [14]: print(my_post)
<QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
오류가 안납니다! filter
는 결과값이 없을 수도 있고, 여러 개일 수도 있을 때 사용합니다. 만약 제목이 ‘Django’인 포스팅을 필터링 한다면?
In [15]: print(Post.objects.filter(title='Django'))
<QuerySet []>
Django
로 시작하는 포스팅은 2개나 있는데, title
로 필터링 하니까 결과가 나오지 않아요. 왜냐하면 title
이 완벽히 ‘Django’인 포스팅은 없거든요. 만약 이 키워드를 포함한 포스팅들을 검색하려면 어떻게 해야할까요?
In [16]: print(Post.objects.filter(title__contains='Django'))
<QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
짜잔 새로운 문법이 나왔습니다. 문법은 title
이라는 attribute
로 조건을 줄 건데, 다음 조건을 포함(contains)하는 데이터들을 가져와라~! 하는 의미에요. attribute
에 조건을 더하기 위해 언더바_
두 개__
를 붙이기로 약속했습니다. DjangoGirls에 따르면,
title와 contains 사이에 있는 밑줄(_)이 2개(__)입니다. 장고 ORM은 필드 이름(“title”)과 연산자과 필터(“contains”)를 밑줄 2개를 사용해 구분합니다. 밑줄 1개만 입력한다면, FieldError: Cannot resolve keyword title_contains라는 오류가 뜰 거예요.
라고 알려주네요! 신기하죠? 아주 다양한 조건들을 줄 수 있어요. QuerySet
조건이 궁금하다면 QuerySet API Reference를 참고해봅시다 :) 양은 짱짱 길어요.
잠깐만요. 우리가 너무 자연스럽게 User
라는 model을 사용하고 있지 않나요? 혹시 지난 번 Post
model을 작성할 때 author
에 어떤 데이터가 들어갔는지, 기억하나요? 기억이 나지 않는다면, 프로젝트에서 blog/models.py
를 열어봅시다.
from django.conf import settings
...
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
...
author
attribute
는 우리가 만든 class
가 아니라 django.conf.settings
에 정의되어 있는, Key 이름이 AUTH_USER_MODEL
인 뭔지 알 수 없지만 어떤 class 타입을 가집니다. 그때 이 타입이 바로 이번 시간에 나타난
In [11]: from django.contrib.auth.models import User
이거였네요! 언젠가 까볼 수 있었으면… 이 말을 왜 꺼냈을까요?
하고 싶었던 이야기는, Post
라는 모델을 작성할 때 저 User
라는 타입을 그대로 가져다 쓰지 않고 ForeignKey
로 연결해서 사용했다는 점이에요. Database에 대한 이해를 가지고 있으신 분들이라면 아마도 알고 계실 수 있지만, 모르신다면 Database
의 Primary Key
와 Foreign Key
개념 정도는 확인해주시는 것도, 좋을 것 같아요 :)
자 우리의 최종 목표가 뭐였는지 기억하시나요? 바로 발행날짜(published_date
)가 최신인 포스팅들을 보여주기 였어요. 그러면 이제 발행된 포스팅들을 불러와봅시다.
In [12]: from django.utils import timezone
# lte 는 less than equal의 약자입니다.
In [13]: Post.objects.filter(published_date__lte=timezone.now())
Out[13]: <QuerySet []>
음? 아무 데이터도 없네요? 관리자 페이지에서 데이터를 다시 살펴보죠.
published_date
가 비어있네요. Post
model을 만들 때 published_date
attribute
를 blank=True, null=True
로 주었던 거 기억하세요? 그렇기 때문에 Post
객체에 published_date
는 값이 있을 수도, 없을 수도 있답니다.(그걸 우리는 nullable하다고 합니다.)
발행된 포스팅들만 보여주어야 하니까 포스팅에 publish()
method
를 호출해봅시다.
all_posts
에서 첫 번째 post
만 발행하려면?
In [14]: all_posts[0].publish()
In [15]: Post.objects.filter(published_date__lte=timezone.now())
Out[15]: <QuerySet [<Post: Django 스터디 Day 1>]>
짠! 신기하지 않나요? 여러분 all_post
의 Type
이 뭐였죠?
In [6]: print(all_posts)
<QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
all_post
는 QuerySet class
의 instance
인데, python
의 List
처럼 all_posts[0]
문법으로 데이터에 접근할 수 있습니다. 왤까요???
QuerySet
은 내부에서 data를 List
형태로 가지고 있기 때문에, all_posts[0]
요청이 들어오면 가지고 있는 data에서 0번째에 있는 데이터를 넘겨줍니다!
짱 편하다~!!
자 이번에는 새로운 포스팅을 코드로 추가해봅시다!
In [16]: newPost = Post.objects.create(author=me, title='caution과 함께하는 Django Study', text='재밌당!')
In [17]: newPost.publish()
짠짠 매우 쉽죠? 새로운 Post
를 만들기 위해 필요한 기본 데이터들을 넣어주고, publish()
로 바로 발행해주었습니다~
음음, 이제 데이터를 원하는 기준으로 정렬할 건데요, 먼저 만들어진 날짜를 기준으로 오래된 순으로 정렬을 해봅시다.
# order_by('조건이 될 attribute 이름')
In [18]: Post.objects.order_by('created_date')
Out[18]: <QuerySet [<Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>, <Post: caution과 함께하는 Django Study>>
별도 조건을 주지 않으면 날짜가 오래된 순 부터 출력됩니다. 만약 최신순으로 정렬하고 싶다면요?
# 내림차순으로 정렬하려면 -를 붙여줍니다.
In [19]: Post.objects.order_by('-created_date')
Out[19]: <QuerySet [<Post: caution과 함께하는 Django Study>, <Post: Django 스터디 Day 2>, <Post: Django 스터디 Day 1>]>
published_date
로 정렬하면 어떨까요? 아까 filter
를 사용할 때는 published_date
가 null이면 보여지지 않았어요.
In [20]: Post.objects.order_by('published_date')
Out[20]: <QuerySet [<Post: Django 스터디 Day 2>, <Post: caution과 함께하는 Django Study>, <Post: Django 스터디 Day 1>]>
오, published_date
가 없는 경우에는 맨 앞에 나오네요. 그럼 내림차순으로 할 때는 결과가 다를까요?
In [21]: Post.objects.order_by('-published_date')
Out[21]: <QuerySet [<Post: caution과 함께하는 Django Study>, <Post: Django 스터디 Day 1>, <Post: Django 스터디 Day 2>]>
오오… 내림차순일 땐 published_date
가 없는 경우가 맨 뒤로 가네요. 조심해야겠는걸요?
우리의 최종목표인 발행날짜(published_date
)가 최신인 포스팅들을 보여주기 에 거의 다 온 것 같아요. 하지만, 발행도 되지 않은 포스팅을 보여줄 필요가 있을까요?
그래서 두 개의 조건을 결합하려고 해요.
published_date
가 null이 아닌 포스팅)published_date
로 내림차순)이 조건을 코드로 짜보면?
In [22]: Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date')
Out[22]: <QuerySet [<Post: caution과 함께하는 Django Study>, <Post: Django 스터디 Day 1>]>
와! 드디어 우리가 원하는 데이터를 가져왔어요 :)
원하는 만큼 데이터를 가지고 놀았으니, 이제 그만 shell
을 종료할 시간입니다.
In [23]: exit()
쨔잔~ 이제 원하는 데이터를 가져왔다면 이걸 화면에서 보여주고 싶어요! 그러려면 데이터(model
)와 화면(template
)을 연결하는 작업이 필요합니다~
이 역할을 Django
에서 view
가 하고 있습니다. blog/views.py
파일을 열어보죠.
post_list(request)
가 호출되면 post_list.html
파일을 response
로 넘겨주는 코드가 작성되어 있을 거에요. 하지만 이제 단순히 HTML
파일을 넘겨주지 않고, 이 템플릿 파일에 데이터를 연결해서 넘겨줄거에요.
먼저 그러기 위해선 model
을 불러와야 합니다.
from django.http import HttpResponse
from django.shortcuts import render
# 이 부분이 추가됐어요!
from .models import Post
def post_list(request):
return render(request, 'post_list.html')
from 다음에 있는 마침표(.)는 현재 디렉토리 또는 애플리케이션을 의미합니다. 동일한 디렉터리 내 views.py, models.py파일이 있기 때문에 . 파일명 (.py확장자를 붙이지 않아도)으로 내용을 가져올 수 있습니다.
(출처: DjangoGirls)
이제 post_list
method
내부에서 우리가 필요한 데이터를 가져와서 posts
라는 변수에 담아볼게요. 위에서 사용했던 QuerySet
을 그대로 사용할 거에요!
from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone
from .models import Post
def post_list(request):
posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date')
context = {'post_list': posts}
return render(request, 'post_list.html', context)
그리고 나서, post_lisht.html
을 기반으로 response
를 render
할 때 사용할 데이터를 Key-Value
데이터 타입인 Dictionary
형태로 넘겨줄거에요. 데이터는 context
라는 변수에 담아서 넘겨주는 문법입니다.
자! 이제 다시 post-list 화면에 들어가 볼까요?
이전과 바뀐게 있다면 시력을 의심해보아야할 겁니다~!
renderer가 response를 만들 때 사용할 수 있도록 data를 넘겨주긴 했지만, 실제 템플릿에서 이 데이터를 어떻게 사용할 것인지에 대해서는 알려주지 않았어요. 그래서 이제부터 해야할 일은 템플릿에서 데이터를 가지고 화면을 그리는 일을 할거에요!
이제 templates/post_list.html
파일을 열어봅시다.
쉬운 것부터 해보자구요. 받아온 데이터를 그대로 출력해보는 건 어때요?
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>post list</h1>
{{ post_list }}
</body>
</html>
post_list
화면을 새로고침해봅시다!
오우. 예쁘진 않지만 데이터가 잘 나오고 있네요. HTML
Template 내부에서 {{ 변수명 }}
을 사용하면 선언된 변수에 접근할 수 있습니다.
그럼 이제 좀 더 예쁘게 볼 수 있도록 html 코드를 추가해볼게요.
...
<body>
<h1>post list</h1>
{% for post in post_list %}
<div>
<p>published: {{ post.published_date }}</p>
<h1><a href="">{{ post.title }}</a></h1>
<p>{{ post.text|linebreaksbr }}</p>
</div>
{% endfor %}
</body>
...
문법을 잠깐 볼게요. {% %}
이게 갑자기 나타났어요. 요건 위에서 {{ }}
사용한 것과 마찬가지로, 이 부분은 html 요소가 아니라 별도 코드로 해석되어야 하는 부분이야~ 하고 알려주는 거에요. 흔히들 이걸 Template Language라고 부릅니다.
각 웹프로그래밍 언어별로 사용되는 Template Language의 형태가 다를 수 있어요. Django
에서 사용하는 방식을 알아보려면? The Django template language를 참고하세요!
자 이제 화면을 새로고침해봅시다! 어떤가요?
엄청엄청 예쁜 것 까진 아니지만, 이렇게 나오면 성공입니다! XD
오늘 포스팅은 여기까지에요 헤헤헤헤헤
다음 시간에 뵈어용! 긴 포스팅 읽어주셔서 감사합니다. XD