2026년 iOS 접근성 면접 질문: VoiceOver와 Dynamic Type
iOS 면접 대비를 위한 핵심 접근성 질문: VoiceOver, Dynamic Type, 시맨틱 traits, 접근성 감사.

접근성은 iOS 개발에서 핵심 역량으로 자리 잡았습니다. 채용 담당자는 VoiceOver, Dynamic Type, 접근성 API에 대한 숙련도를 체계적으로 평가합니다. 이 글에서는 기술 면접에서 성공하기 위한 필수 개념을 정리합니다.
전 세계에는 10억 명이 넘는 장애인이 살고 있습니다. Apple은 엄격한 접근성 기준을 시행하고 있으며, 많은 회사는 접근성이 떨어지는 앱의 출시를 거부합니다. 이 역량은 시니어 후보자를 다른 후보와 구분 짓습니다.
iOS 접근성 기초
1. iOS의 주요 접근성 도구는 무엇입니까?
iOS는 다양한 장애 유형을 지원하는 포괄적인 접근성 도구를 제공합니다. VoiceOver는 시각장애 사용자에게 화면을 읽어 줍니다. Dynamic Type은 텍스트 크기를 조정합니다. Switch Control은 외부 스위치를 통한 조작을 지원합니다. Voice Control은 완전한 음성 명령을 제공합니다.
// Check if VoiceOver is running
if UIAccessibility.isVoiceOverRunning {
// Adapt the interface for VoiceOver
showSimplifiedInterface()
}
// Check if Reduce Motion is enabled
if UIAccessibility.isReduceMotionEnabled {
// Disable complex animations
animationDuration = 0
}
// Check if Bold Text is enabled
if UIAccessibility.isBoldTextEnabled {
// Use heavier fonts
applyBoldFonts()
}
// Observe settings changes
NotificationCenter.default.addObserver(
forName: UIAccessibility.voiceOverStatusDidChangeNotification,
object: nil,
queue: .main
) { _ in
// React to the change
updateAccessibilityLayout()
}이러한 API는 사용자 설정과 활성화된 보조 기술에 따라 인터페이스를 동적으로 조정합니다.
2. VoiceOver는 기술적으로 어떻게 동작합니까?
VoiceOver는 인터페이스를 순회하며 접근 가능한 요소를 식별합니다. 각 요소는 label, value, traits, action 같은 속성을 노출합니다. 스크린 리더는 사용자가 제스처로 이동할 때 이 정보를 음성으로 합성합니다.
// In UIKit: configure an accessible element
class CustomButton: UIButton {
override var accessibilityLabel: String? {
get { "Validation button" } // What VoiceOver reads
set { }
}
override var accessibilityHint: String? {
get { "Double-tap to confirm the order" } // Possible action
set { }
}
override var accessibilityTraits: UIAccessibilityTraits {
get { .button } // Element type
set { }
}
override var isAccessibilityElement: Bool {
get { true } // Element focusable by VoiceOver
set { }
}
}
// In SwiftUI: equivalent with modifiers
struct ValidateButton: View {
var body: some View {
Button("Validate") {
confirmOrder()
}
.accessibilityLabel("Validation button")
.accessibilityHint("Double-tap to confirm the order")
}
}접근성 계층은 시각적 계층과 다를 수 있어, 복잡한 레이아웃에서도 논리적인 탐색이 가능합니다.
3. accessibilityLabel과 accessibilityValue의 차이는?
accessibilityLabel은 요소를 영구적으로 식별하고, accessibilityValue는 변하는 현재 상태를 나타냅니다. 슬라이더나 스위치 같은 동적 컨트롤에서는 이 구분이 매우 중요합니다.
// Example with a volume slider
class VolumeSlider: UISlider {
override var accessibilityLabel: String? {
get { "Volume" } // Fixed element identity
set { }
}
override var accessibilityValue: String? {
get { "\(Int(value * 100)) percent" } // Current state that changes
set { }
}
}
// VoiceOver reads: "Volume, 75 percent"
// In SwiftUI with a Toggle
struct NotificationToggle: View {
@Binding var isEnabled: Bool
var body: some View {
Toggle(isOn: $isEnabled) {
Text("Notifications")
}
.accessibilityLabel("Push notifications")
// accessibilityValue is automatic for Toggle: "on" or "off"
}
}
// For a custom stepper
struct QuantityStepper: View {
@State private var quantity = 1
var body: some View {
Stepper("Quantity", value: $quantity, in: 1...99)
.accessibilityLabel("Item quantity")
.accessibilityValue("\(quantity) item\(quantity > 1 ? "s" : "")")
}
}VoiceOver는 항상 label에 이어 value를 알려주므로, 사용자는 요소가 무엇이고 현재 상태는 어떠한지 함께 파악할 수 있습니다.
4. accessibilityTraits와 그 중요성을 설명하십시오
Traits는 VoiceOver에 요소의 종류와 동작을 알려 줍니다. 요소가 어떻게 안내되고 어떤 동작이 가능한지를 결정합니다. .button trait이 없는 버튼은 버튼으로 안내되지 않습니다.
// Common traits in UIKit
class CustomCell: UITableViewCell {
override var accessibilityTraits: UIAccessibilityTraits {
get {
var traits: UIAccessibilityTraits = []
// Element type
traits.insert(.button) // Interactive element
// Current state
if isSelected {
traits.insert(.selected) // Currently selected
}
// Special characteristics
if !isEnabled {
traits.insert(.notEnabled) // Disabled
}
return traits
}
set { }
}
}
// Available traits
// .button - Clickable element
// .link - Hyperlink
// .header - Section header
// .image - Image
// .selected - Selected state
// .notEnabled - Disabled
// .adjustable - Adjustable value (slider)
// .staticText - Non-interactive text
// .searchField - Search field
// .playsSound - Plays a sound
// .startsMediaSession - Starts media playback
// In SwiftUI
struct SectionHeader: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.accessibilityAddTraits(.isHeader) // Announced as header
}
}
struct SelectableRow: View {
let isSelected: Bool
var body: some View {
HStack {
Text("Option")
if isSelected {
Image(systemName: "checkmark")
}
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}Traits 덕분에 보조 기술은 사용자에게 일관되고 예측 가능한 경험을 제공합니다.
Dynamic Type와 가변 텍스트 크기
5. Dynamic Type을 올바르게 구현하는 방법은?
Dynamic Type은 사용자가 시스템 텍스트 크기를 조정할 수 있게 해 줍니다. 시맨틱 텍스트 스타일과 유연한 제약을 사용해 인터페이스가 자동으로 적응하도록 구현합니다.
// In UIKit: use preferred text styles
class ArticleCell: UITableViewCell {
let titleLabel = UILabel()
let bodyLabel = UILabel()
func configure() {
// Use semantic text styles
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
// CRUCIAL: enable automatic adjustment
titleLabel.adjustsFontForContentSizeCategory = true
bodyLabel.adjustsFontForContentSizeCategory = true
// Allow multiline for large sizes
titleLabel.numberOfLines = 0
bodyLabel.numberOfLines = 0
}
}
// In SwiftUI: it's automatic with styles
struct ArticleView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Article Title")
.font(.headline) // Adapts automatically
Text("Article content...")
.font(.body)
}
}
}
// Custom font with Dynamic Type
extension UIFont {
static func customFont(
size: CGFloat,
style: TextStyle
) -> UIFont {
let customFont = UIFont(name: "CustomFont-Regular", size: size)!
// Adapt to current size category
return UIFontMetrics(forTextStyle: style)
.scaledFont(for: customFont)
}
}adjustsFontForContentSizeCategory를 활성화하지 않으면 사용자가 설정을 바꿔도 라벨은 처음 크기를 유지합니다.
6. 극단적인 접근성 크기를 어떻게 다룹니까?
접근성 크기(AX1부터 AX5)는 텍스트 크기를 세 배까지 키울 수 있습니다. 인터페이스는 가로 정렬을 세로로 바꾸고, 장식 요소를 숨기며, 간격을 조정하는 등 대체 레이아웃으로 적응해야 합니다.
// Detect accessibility sizes
struct AdaptiveLayout: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
// Different layout for large sizes
if dynamicTypeSize.isAccessibilitySize {
// Vertical layout for large sizes
VStack(alignment: .leading, spacing: 12) {
iconAndTitle
actionButtons
}
} else {
// Standard horizontal layout
HStack {
iconAndTitle
Spacer()
actionButtons
}
}
}
var iconAndTitle: some View {
HStack {
Image(systemName: "bell.fill")
.accessibilityHidden(true) // Decorative, ignore
Text("Notifications")
}
}
var actionButtons: some View {
HStack(spacing: 16) {
Button("Enable") { }
Button("Settings") { }
}
}
}
// In UIKit: observe changes
class AdaptiveViewController: UIViewController {
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
// Check if size category changed
if traitCollection.preferredContentSizeCategory !=
previousTraitCollection?.preferredContentSizeCategory {
updateLayoutForContentSize()
}
}
func updateLayoutForContentSize() {
let category = traitCollection.preferredContentSizeCategory
if category.isAccessibilityCategory {
// Enable accessible layout
stackView.axis = .vertical
decorativeView.isHidden = true
} else {
// Standard layout
stackView.axis = .horizontal
decorativeView.isHidden = false
}
}
}접근성 카테고리는 .accessibilityMedium(AX1)부터 시작합니다. 디자인은 처음부터 이런 케이스를 예상해야 합니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
7. 이미지를 어떻게 접근 가능하게 만듭니까?
이미지는 역할에 따라 다르게 다뤄야 합니다. 정보형 이미지에는 설명이 필요하고, 장식 이미지는 무시되어야 하며, 상호작용 이미지에는 행동을 나타내는 라벨이 필요합니다.
// Informative image: describe the content
struct ProductImage: View {
let product: Product
var body: some View {
AsyncImage(url: product.imageURL) { image in
image
.resizable()
.accessibilityLabel(product.imageDescription)
// "Product photo: iPhone 15 Pro, titanium color"
} placeholder: {
ProgressView()
.accessibilityLabel("Loading image")
}
}
}
// Decorative image: hide from VoiceOver
struct DecorationView: View {
var body: some View {
Image(systemName: "sparkles")
.accessibilityHidden(true) // Ignored by VoiceOver
}
}
// Image button: describe the action, not the image
struct FavoriteButton: View {
@Binding var isFavorite: Bool
var body: some View {
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}
// Don't describe the icon, describe the action
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
}
}
// In UIKit
class ProductImageView: UIImageView {
override var isAccessibilityElement: Bool {
get { true }
set { }
}
func configure(with product: Product) {
image = product.image
accessibilityLabel = "Photo: \(product.name), \(product.color)"
}
}
// Image with embedded text (infographic)
class InfographicView: UIImageView {
override var accessibilityLabel: String? {
get {
// Transcribe the textual content of the infographic
"""
Statistics infographic 2026.
Growth: 45%.
Active users: 2.3 million.
Satisfaction: 4.8 out of 5.
"""
}
set { }
}
}설명이 부실한 이미지나 가려지지 않은 장식 이미지는 VoiceOver 경험을 크게 떨어뜨립니다.
그룹화와 고급 탐색
8. VoiceOver를 위해 요소를 어떻게 그룹화합니까?
그룹화는 여러 시각 요소를 하나의 접근 가능한 요소로 결합합니다. 이는 탐색 속도를 높이고 한 번의 안내로 전체 맥락을 제공합니다.
// In SwiftUI: combine children
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text(product.price)
.font(.subheadline)
HStack {
Image(systemName: "star.fill")
Text(product.rating)
}
}
// Combines all text into a single element
.accessibilityElement(children: .combine)
// VoiceOver reads: "iPhone 15 Pro, $1199, 4.8 stars"
}
}
// In SwiftUI: ignore children and define manually
struct OrderSummary: View {
let order: Order
var body: some View {
HStack {
Image(systemName: "bag")
VStack(alignment: .leading) {
Text(order.itemCount)
Text(order.total)
}
Image(systemName: "chevron.right")
}
// Ignore children hierarchy
.accessibilityElement(children: .ignore)
// Define custom label
.accessibilityLabel("Order of \(order.itemCount) items for \(order.total)")
.accessibilityHint("Double-tap to view details")
.accessibilityAddTraits(.isButton)
}
}
// In UIKit: accessible container
class ProductCardView: UIView {
let nameLabel = UILabel()
let priceLabel = UILabel()
let ratingLabel = UILabel()
override var isAccessibilityElement: Bool {
get { true } // The container is the accessible element
set { }
}
override var accessibilityLabel: String? {
get {
// Combine information
"\(nameLabel.text ?? ""), \(priceLabel.text ?? ""), \(ratingLabel.text ?? "")"
}
set { }
}
}그룹화하지 않으면 VoiceOver가 모든 Text마다 멈춰 탐색이 크게 느려집니다.
9. 사용자 정의 접근성 액션은 어떻게 구현합니까?
사용자 정의 액션은 시각적 UI를 어지럽히지 않고 접근 가능한 기능을 추가합니다. VoiceOver 사용자는 세로 스와이프로 이를 사용합니다.
// In SwiftUI: custom actions
struct EmailRow: View {
let email: Email
let onArchive: () -> Void
let onDelete: () -> Void
let onFlag: () -> Void
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(email.sender)
.font(.headline)
Text(email.subject)
.font(.subheadline)
}
Spacer()
if email.isFlagged {
Image(systemName: "flag.fill")
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(email.sender), \(email.subject)")
// Actions accessible via vertical swipe
.accessibilityAction(named: "Archive") {
onArchive()
}
.accessibilityAction(named: "Delete") {
onDelete()
}
.accessibilityAction(named: email.isFlagged ? "Remove flag" : "Flag") {
onFlag()
}
}
}
// In UIKit: override accessibilityCustomActions
class EmailCell: UITableViewCell {
var email: Email!
var onArchive: (() -> Void)?
var onDelete: (() -> Void)?
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
get {
[
UIAccessibilityCustomAction(
name: "Archive",
target: self,
selector: #selector(archiveAction)
),
UIAccessibilityCustomAction(
name: "Delete",
target: self,
selector: #selector(deleteAction)
)
]
}
set { }
}
@objc private func archiveAction() -> Bool {
onArchive?()
return true // true = action succeeded
}
@objc private func deleteAction() -> Bool {
onDelete?()
return true
}
}사용자 정의 액션은 VoiceOver에서 발견하기 어려운 스와이프 제스처를 효과적으로 대체합니다.
10. VoiceOver 포커스를 프로그래밍 방식으로 어떻게 관리합니까?
인터페이스 변경(모달 등장, 콘텐츠 로딩, 화면 이동) 후 포커스를 제어하는 일은 매우 중요합니다. 포커스는 사용자를 의미 있는 정보로 안내해야 합니다.
// In SwiftUI with @AccessibilityFocusState (iOS 15+)
struct SearchView: View {
@State private var searchText = ""
@State private var results: [Result] = []
@AccessibilityFocusState private var focusedResult: Result?
var body: some View {
VStack {
TextField("Search", text: $searchText)
.onSubmit {
performSearch()
}
List(results) { result in
ResultRow(result: result)
.accessibilityFocused($focusedResult, equals: result)
}
}
}
func performSearch() {
Task {
results = await api.search(searchText)
// Move focus to first result
if let first = results.first {
focusedResult = first
}
}
}
}
// In UIKit: post a notification
class ModalViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Announce and move focus
UIAccessibility.post(
notification: .screenChanged, // New screen
argument: titleLabel // Element to focus
)
}
func showError(_ message: String) {
errorLabel.text = message
errorLabel.isHidden = false
// Announce layout change
UIAccessibility.post(
notification: .layoutChanged, // Change in current screen
argument: errorLabel
)
}
func showLoadingComplete() {
// Announce without moving focus
UIAccessibility.post(
notification: .announcement,
argument: "Loading complete"
)
}
}.screenChanged와 .layoutChanged 알림은 VoiceOver에 인터페이스의 중요한 변경을 알립니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
조정 가능한 요소와 복잡한 컨트롤
11. 사용자 정의 슬라이더를 접근 가능하게 만들려면?
사용자 정의 슬라이더는 .adjustable trait을 구현하고 증가/감소 제스처에 응답해야 합니다. VoiceOver는 세로 스와이프로 값을 변경합니다.
// In SwiftUI: Slider is accessible by default
struct VolumeControl: View {
@Binding var volume: Double
var body: some View {
Slider(value: $volume, in: 0...100, step: 5)
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(volume)) percent")
}
}
// Custom accessible slider
struct CustomRatingSlider: View {
@Binding var rating: Int
var body: some View {
HStack {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.onTapGesture { rating = star }
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) star\(rating > 1 ? "s" : "") out of 5")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
rating = min(5, rating + 1)
case .decrement:
rating = max(1, rating - 1)
@unknown default:
break
}
}
}
}
// In UIKit: adjustable trait
class StarRatingView: UIView {
var rating: Int = 3 {
didSet {
updateStars()
// Announce the new value
UIAccessibility.post(
notification: .announcement,
argument: accessibilityValue
)
}
}
override var accessibilityTraits: UIAccessibilityTraits {
get { .adjustable } // Enables adjustment gestures
set { }
}
override var accessibilityLabel: String? {
get { "Rating" }
set { }
}
override var accessibilityValue: String? {
get { "\(rating) stars out of 5" }
set { }
}
// Called on swipe up
override func accessibilityIncrement() {
rating = min(5, rating + 1)
}
// Called on swipe down
override func accessibilityDecrement() {
rating = max(1, rating - 1)
}
}.adjustable trait이 없다면 사용자 정의 슬라이더는 사용자가 별 하나하나를 터치해야 합니다.
12. 접근 가능한 캐러셀을 어떻게 만듭니까?
캐러셀은 비선형 탐색, 화면 밖 콘텐츠, 다중 상태 같은 접근성 과제를 안고 있습니다. 해결책은 .adjustable trait과 맥락에 맞는 안내를 결합하는 것입니다.
struct ImageCarousel: View {
let images: [CarouselImage]
@State private var currentIndex = 0
var body: some View {
TabView(selection: $currentIndex) {
ForEach(images.indices, id: \.self) { index in
CarouselItem(image: images[index])
.tag(index)
}
}
.tabViewStyle(.page)
.accessibilityElement(children: .contain)
.accessibilityLabel("Image carousel")
.accessibilityValue("Image \(currentIndex + 1) of \(images.count)")
.accessibilityHint("Swipe to change image")
.accessibilityAdjustableAction { direction in
withAnimation {
switch direction {
case .increment:
currentIndex = min(images.count - 1, currentIndex + 1)
case .decrement:
currentIndex = max(0, currentIndex - 1)
@unknown default:
break
}
}
}
}
}
// In UIKit with UIPageViewController
class CarouselAccessibilityContainer: UIView {
var currentPage = 0
var totalPages = 5
override var isAccessibilityElement: Bool {
get { true }
set { }
}
override var accessibilityTraits: UIAccessibilityTraits {
get { .adjustable }
set { }
}
override var accessibilityLabel: String? {
get { "Promotional carousel" }
set { }
}
override var accessibilityValue: String? {
get { "Page \(currentPage + 1) of \(totalPages)" }
set { }
}
override func accessibilityIncrement() {
guard currentPage < totalPages - 1 else { return }
currentPage += 1
delegate?.scrollToPage(currentPage)
}
override func accessibilityDecrement() {
guard currentPage > 0 else { return }
currentPage -= 1
delegate?.scrollToPage(currentPage)
}
}이러한 접근으로 표준 스와이프에 의존하지 않고도 캐러셀을 탐색할 수 있습니다.
13. 폼 접근성은 어떻게 다룹니까?
접근 가능한 폼은 각 필드를 라벨과 연결하고, 오류를 명확히 표시하며, 필드 간 부드러운 이동을 보장합니다.
struct RegistrationForm: View {
@State private var email = ""
@State private var password = ""
@State private var emailError: String?
@State private var passwordError: String?
var body: some View {
Form {
Section {
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.accessibilityLabel("Email address")
.accessibilityHint(emailError != nil ? "Error: \(emailError!)" : nil)
if let error = emailError {
Text(error)
.foregroundColor(.red)
.font(.caption)
.accessibilityLabel("Email error: \(error)")
}
}
Section {
SecureField("Password", text: $password)
.textContentType(.newPassword)
.accessibilityLabel("Password")
.accessibilityHint(passwordHint)
PasswordStrengthIndicator(password: password)
}
Button("Create account") {
submitForm()
}
.disabled(!isFormValid)
.accessibilityHint(isFormValid ? "Double-tap to create your account" : "Form incomplete")
}
}
var passwordHint: String {
if let error = passwordError {
return "Error: \(error)"
}
return "Minimum 8 characters with uppercase and numbers"
}
}
// Accessible password strength indicator
struct PasswordStrengthIndicator: View {
let password: String
var strength: PasswordStrength {
// Calculation logic
PasswordStrength.calculate(password)
}
var body: some View {
HStack(spacing: 4) {
ForEach(0..<4) { index in
Rectangle()
.fill(index < strength.level ? strength.color : Color.gray.opacity(0.3))
.frame(height: 4)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Password strength")
.accessibilityValue(strength.description) // "Weak", "Medium", "Strong", "Very strong"
}
}접근성 hint는 시각 UI를 어지럽히지 않고도 오류와 안내 사항을 전달합니다.
접근성 감사와 테스트
14. iOS 앱의 접근성을 어떻게 감사합니까?
감사는 자동화 도구와 수동 테스트를 결합합니다. Xcode의 Accessibility Inspector는 요소를 분석하고, VoiceOver는 실제 경험을 검증하며, 단위 테스트는 접근성 속성을 확인합니다.
// Unit test accessibility properties
import XCTest
@testable import MyApp
class AccessibilityTests: XCTestCase {
func testProductCardAccessibility() {
let product = Product(name: "iPhone", price: "$999", rating: "4.5")
let view = ProductCardView(product: product)
// Verify element is accessible
XCTAssertTrue(view.isAccessibilityElement)
// Verify label
XCTAssertEqual(
view.accessibilityLabel,
"iPhone, $999, 4.5 stars"
)
// Verify traits
XCTAssertTrue(view.accessibilityTraits.contains(.button))
}
func testDynamicTypeSupport() {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .body)
// Verify automatic adjustment
XCTAssertTrue(label.adjustsFontForContentSizeCategory)
}
}
// UI test with simulated VoiceOver
class AccessibilityUITests: XCTestCase {
func testLoginFlowAccessibility() {
let app = XCUIApplication()
app.launch()
// Access elements by accessibility label
let emailField = app.textFields["Email address"]
XCTAssertTrue(emailField.exists)
emailField.tap()
emailField.typeText("test@example.com")
let passwordField = app.secureTextFields["Password"]
XCTAssertTrue(passwordField.exists)
let loginButton = app.buttons["Sign in"]
XCTAssertTrue(loginButton.exists)
XCTAssertTrue(loginButton.isEnabled)
}
// Automatic audit iOS 17+
func testAccessibilityAudit() throws {
let app = XCUIApplication()
app.launch()
// Automatic screen audit
try app.performAccessibilityAudit()
}
}iOS 17 이상의 자동 감사는 라벨 누락, 대비 부족, 너무 작은 요소 등 흔한 문제를 찾아냅니다.
15. 가장 흔한 접근성 실수는?
흔한 실수에는 라벨 누락, 설명 없는 이미지, Dynamic Type 미지원, 적절한 traits이 없는 인터랙션 요소 등이 포함됩니다.
// WRONG: Button without accessible label
Button {
performAction()
} label: {
Image(systemName: "plus")
}
// VoiceOver reads: "Button" - useless
// CORRECT: Explicit label
Button {
performAction()
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("Add item")
// WRONG: Fixed font size
Text("Important")
.font(.system(size: 16)) // Doesn't respect Dynamic Type
// CORRECT: Semantic text style
Text("Important")
.font(.body) // Adapts to preferences
// WRONG: Informative image without description
Image("product-photo")
// CORRECT: Content description
Image("product-photo")
.accessibilityLabel("Product photo: Black audio headphones")
// WRONG: Clickable element without button trait
Text("Learn more")
.onTapGesture { showDetails() }
// VoiceOver doesn't know it's interactive
// CORRECT: Appropriate traits
Text("Learn more")
.onTapGesture { showDetails() }
.accessibilityAddTraits(.isButton)
.accessibilityHint("Double-tap to show details")
// WRONG: Insufficient contrast
Text("Light gray text")
.foregroundColor(.gray.opacity(0.4))
// CORRECT: Minimum contrast ratio 4.5:1
Text("Readable text")
.foregroundColor(.secondary) // Respects guidelines
// WRONG: Touch target too small
Button { } label: {
Image(systemName: "xmark")
.font(.caption) // Too small (< 44x44 points)
}
// CORRECT: Minimum 44x44 point area
Button { } label: {
Image(systemName: "xmark")
}
.frame(minWidth: 44, minHeight: 44)이러한 실수는 Accessibility Inspector로 감지할 수 있고 단 몇 줄의 코드로 수정 가능합니다.
결론
iOS 접근성은 시각장애 사용자를 위한 VoiceOver, 텍스트 적응을 위한 Dynamic Type, 일관된 탐색을 위한 시맨틱 traits이라는 세 기둥에 기반을 둡니다. 이 개념들을 능숙하게 다루는 것은 채용 시장이 적극적으로 찾는 전문 역량을 보여 줍니다.
점검 목록
- VoiceOver를 적절한 라벨, hint, traits로 구성
- 시맨틱 텍스트 스타일로 Dynamic Type 구현
- 극단적 접근성 크기에 맞는 레이아웃 마련
- 효율적 탐색을 위해 요소 그룹화
- 인터페이스 변경 후 포커스를 프로그래밍 방식으로 관리
- 사용자 정의 컨트롤을 조정 가능하도록 구현
- Accessibility Inspector와 자동 테스트로 정기 감사
- 흔한 실수 회피: 라벨 누락, 설명 없는 이미지, 대비 부족
추가 자료
Apple 공식 문서 "Human Interface Guidelines - Accessibility"는 여전히 핵심 참고 자료입니다. VoiceOver를 켜고 자주 테스트하면 자동 도구가 놓치는 문제를 찾는 데 도움이 됩니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Swift에서 Combine vs async/await: 점진적 마이그레이션 패턴
Swift에서 Combine에서 async/await로 마이그레이션하는 완전한 가이드: 점진적 전략, 브리징 패턴, iOS 코드베이스의 패러다임 공존.

Swift Macros: 메타프로그래밍 실전 예제
Swift Macros 완전 가이드: freestanding 및 attached 매크로 작성, swift-syntax를 활용한 AST 조작, 보일러플레이트를 줄이는 실전 예제 제공.

StoreKit 2 인터뷰: 구독 관리 및 영수증 검증
StoreKit 2, 구독 관리, 영수증 검증, 인앱 구매 구현에 관한 iOS 인터뷰 질문을 실용적인 Swift 코드 예제와 함께 마스터하십시오.