สัมภาษณ์ iOS Senior 2026: คำถามเรื่องสถาปัตยกรรมและ Design Pattern

เตรียมตัวสัมภาษณ์ iOS senior ด้วยคำถามสำคัญเกี่ยวกับ MVVM, VIPER, Clean Architecture และ design pattern คู่มือครบถ้วนพร้อมตัวอย่างโค้ด Swift

ไดอะแกรมสถาปัตยกรรม iOS พร้อม pattern MVVM และ VIPER สำหรับการสัมภาษณ์เทคนิคระดับ senior

การสัมภาษณ์ iOS senior ให้ความสำคัญอย่างมากกับสถาปัตยกรรมและ design pattern นอกจากไวยากรณ์ของ Swift แล้ว ผู้สัมภาษณ์ยังประเมินความสามารถในการออกแบบแอปพลิเคชันที่บำรุงรักษาง่าย ทดสอบได้ และขยายตัวได้

คู่มือนี้ครอบคลุมคำถามที่พบบ่อยที่สุดเกี่ยวกับ MVVM, VIPER, Clean Architecture และ pattern ที่จำเป็น พร้อมคำตอบโดยละเอียดและตัวอย่างโค้ดที่พร้อมใช้งานในการสัมภาษณ์

สิ่งที่ผู้สัมภาษณ์ประเมินจริง ๆ

ในการสัมภาษณ์ระดับ senior คำตอบทางเทคนิคสำคัญน้อยกว่าการให้เหตุผล ควรอธิบายเสมอว่าทำไมสถาปัตยกรรมหนึ่งจึงเหมาะกับบริบทหนึ่ง ไม่ใช่เพียงวิธีการนำไปใช้

ทำความเข้าใจสถาปัตยกรรม iOS: ภาพรวม

ก่อนเข้าสู่คำถามเฉพาะ การเข้าใจภูมิทัศน์ของสถาปัตยกรรม iOS เป็นสิ่งจำเป็น แต่ละ pattern แก้ปัญหาที่แตกต่างกันและเหมาะกับบริบทที่ต่างกัน

MVC: pattern ดั้งเดิมของ Apple

MVC (Model-View-Controller) ยังคงเป็น pattern เริ่มต้นของ Apple แต่ประสบปัญหา "Massive View Controllers" ในแอปที่ซับซ้อน

UserViewController.swiftswift
// Typical MVC example showing its limitations
class UserViewController: UIViewController {
    // The ViewController accumulates too many responsibilities
    private var users: [User] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        // View logic
        setupUI()
        // Business logic
        fetchUsers()
        // Navigation logic
        setupNavigationBar()
    }

    private func fetchUsers() {
        // Networking in the VC: anti-pattern
        URLSession.shared.dataTask(with: URL(string: "api/users")!) { data, _, _ in
            // JSON parsing here too...
        }.resume()
    }
}

pattern นี้กลายเป็นปัญหาเมื่อ ViewController เกิน 500 บรรทัด ทำให้การทำ unit test แทบเป็นไปไม่ได้

คำถามที่ 1: อธิบาย MVVM และการนำไปใช้ใน Swift

MVVM (Model-View-ViewModel) แยก presentation logic ออกไปยัง ViewModel ทำให้ทดสอบได้ง่ายและลดขนาดของ ViewController

จุดสำคัญในการสัมภาษณ์

MVVM โดดเด่นเมื่อใช้กับ SwiftUI ด้วย @Observable และ data binding ในตัว ส่วน UIKit ต้องใช้กลไก binding (Combine, closure)

การใช้งาน MVVM ด้วย Combine

UserViewModel.swiftswift
// ViewModel separated from any UIKit dependency
import Combine

@MainActor
final class UserViewModel: ObservableObject {
    // Published states for binding
    @Published private(set) var users: [User] = []
    @Published private(set) var isLoading = false
    @Published private(set) var errorMessage: String?

    // Injected dependency for testability
    private let userRepository: UserRepositoryProtocol
    private var cancellables = Set<AnyCancellable>()

    init(userRepository: UserRepositoryProtocol = UserRepository()) {
        self.userRepository = userRepository
    }

    func loadUsers() {
        isLoading = true
        errorMessage = nil

        userRepository.fetchUsers()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] completion in
                self?.isLoading = false
                if case .failure(let error) = completion {
                    self?.errorMessage = error.localizedDescription
                }
            } receiveValue: { [weak self] users in
                self?.users = users
            }
            .store(in: &cancellables)
    }
}

View ที่ใช้ binding ของ Combine

UserListView.swiftswift
// SwiftUI View consuming the ViewModel
import SwiftUI

struct UserListView: View {
    // StateObject for lifecycle management
    @StateObject private var viewModel = UserViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView("Loading...")
            } else if let error = viewModel.errorMessage {
                ErrorView(message: error, retry: viewModel.loadUsers)
            } else {
                List(viewModel.users) { user in
                    UserRowView(user: user)
                }
            }
        }
        .onAppear { viewModel.loadUsers() }
    }
}

ViewModel ไม่รู้จัก UIKit หรือ SwiftUI จึงสามารถทำ unit test ได้อย่างเต็มที่

คำถามที่ 2: เมื่อใดควรเลือก VIPER แทน MVVM?

VIPER (View-Interactor-Presenter-Entity-Router) เหมาะกับแอปที่ซับซ้อนซึ่งต้องการการแยกหน้าที่อย่างเข้มงวดและการนำทางขั้นสูง

โครงสร้าง VIPER แบบเต็ม

UserListProtocols.swiftswift
// Contract definitions between VIPER components
protocol UserListViewProtocol: AnyObject {
    var presenter: UserListPresenterProtocol? { get set }
    func showUsers(_ users: [UserViewModel])
    func showError(_ message: String)
    func showLoading()
}

protocol UserListPresenterProtocol: AnyObject {
    var view: UserListViewProtocol? { get set }
    var interactor: UserListInteractorInputProtocol? { get set }
    var router: UserListRouterProtocol? { get set }
    func viewDidLoad()
    func didSelectUser(_ user: UserViewModel)
}

protocol UserListInteractorInputProtocol: AnyObject {
    var presenter: UserListInteractorOutputProtocol? { get set }
    func fetchUsers()
}

protocol UserListInteractorOutputProtocol: AnyObject {
    func didFetchUsers(_ users: [User])
    func didFailWithError(_ error: Error)
}

protocol UserListRouterProtocol: AnyObject {
    func navigateToUserDetail(with userId: String)
}

Presenter ทำหน้าที่ประสานงานลอจิก

UserListPresenter.swiftswift
// The Presenter bridges View and Interactor
final class UserListPresenter: UserListPresenterProtocol {
    weak var view: UserListViewProtocol?
    var interactor: UserListInteractorInputProtocol?
    var router: UserListRouterProtocol?

    func viewDidLoad() {
        view?.showLoading()
        interactor?.fetchUsers()
    }

    func didSelectUser(_ user: UserViewModel) {
        router?.navigateToUserDetail(with: user.id)
    }
}

// Extension for Interactor callbacks
extension UserListPresenter: UserListInteractorOutputProtocol {
    func didFetchUsers(_ users: [User]) {
        // Model -> ViewModel transformation
        let viewModels = users.map { UserViewModel(from: $0) }
        view?.showUsers(viewModels)
    }

    func didFailWithError(_ error: Error) {
        view?.showError(error.localizedDescription)
    }
}
คำเตือนระหว่างสัมภาษณ์

VIPER เพิ่ม boilerplate จำนวนมาก ควรอธิบายเหตุผลของการใช้ด้วยขนาดทีม (มีนักพัฒนาหลายคนทำงานในโมดูลเดียว) หรือความซับซ้อนของโดเมนธุรกิจ

คำถามที่ 3: นำ Clean Architecture ไปใช้บน iOS อย่างไร?

Clean Architecture จัดเรียงโค้ดเป็นวงกลมซ้อนกัน โดยมีกฎทางธุรกิจอยู่ตรงกลาง เป็นอิสระจากเฟรมเวิร์ก

โครงสร้างแบ่งชั้น

Domain/Entities/User.swiftswift
// Pure business entity, no framework dependencies
struct User: Identifiable, Equatable {
    let id: String
    let email: String
    let fullName: String
    let subscriptionLevel: SubscriptionLevel

    enum SubscriptionLevel: String {
        case free, premium, enterprise
    }
}

// Domain/UseCases/GetUsersUseCase.swift
// Use Case encapsulating a business rule
protocol GetUsersUseCaseProtocol {
    func execute() async throws -> [User]
}

final class GetUsersUseCase: GetUsersUseCaseProtocol {
    // Dependency on abstraction, not implementation
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    func execute() async throws -> [User] {
        let users = try await repository.fetchAll()
        // Business rule: sort by subscription level
        return users.sorted { $0.subscriptionLevel.rawValue > $1.subscriptionLevel.rawValue }
    }
}

ชั้นข้อมูลด้วย pattern Repository

Data/Repositories/UserRepository.swiftswift
// Concrete repository implementation
final class UserRepository: UserRepositoryProtocol {
    private let remoteDataSource: UserRemoteDataSourceProtocol
    private let localDataSource: UserLocalDataSourceProtocol

    init(
        remoteDataSource: UserRemoteDataSourceProtocol = UserRemoteDataSource(),
        localDataSource: UserLocalDataSourceProtocol = UserLocalDataSource()
    ) {
        self.remoteDataSource = remoteDataSource
        self.localDataSource = localDataSource
    }

    func fetchAll() async throws -> [User] {
        do {
            // Cache-first strategy with fallback
            let remoteUsers = try await remoteDataSource.fetchUsers()
            await localDataSource.save(remoteUsers)
            return remoteUsers
        } catch {
            // Fallback to local cache
            return try await localDataSource.fetchUsers()
        }
    }
}

การจัดระเบียบนี้ทำให้สามารถทดสอบแต่ละชั้นได้อย่างอิสระ และเปลี่ยนการนำไปใช้ (เช่น ย้ายจาก CoreData ไปยัง SwiftData) โดยไม่กระทบโดเมน

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

คำถามที่ 4: ใช้ design pattern ใดเป็นประจำ?

ผู้สัมภาษณ์คาดหวังความเชี่ยวชาญจากการใช้งานจริง ไม่ใช่การท่องจำทฤษฎี ต่อไปนี้คือ pattern ที่พบบ่อยที่สุดบน iOS

Dependency Injection ด้วย Property Wrappers

DependencyInjection/Container.swiftswift
// Simple and effective injection container
final class DIContainer {
    static let shared = DIContainer()

    private var factories: [String: () -> Any] = [:]

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        let key = String(describing: type)
        factories[key] = factory
    }

    func resolve<T>(_ type: T.Type) -> T {
        let key = String(describing: type)
        guard let factory = factories[key], let instance = factory() as? T else {
            fatalError("No registration for \(key)")
        }
        return instance
    }
}

// Property Wrapper for elegant injection
@propertyWrapper
struct Injected<T> {
    private var value: T

    init() {
        self.value = DIContainer.shared.resolve(T.self)
    }

    var wrappedValue: T {
        get { value }
        mutating set { value = newValue }
    }
}

// Usage in a ViewModel
final class PaymentViewModel {
    @Injected private var paymentService: PaymentServiceProtocol
    @Injected private var analyticsService: AnalyticsServiceProtocol

    func processPayment(_ amount: Decimal) async throws {
        analyticsService.track(.paymentInitiated(amount: amount))
        try await paymentService.charge(amount)
    }
}

Coordinator Pattern สำหรับการนำทาง

Coordinators/AppCoordinator.swiftswift
// Coordinator managing navigation flow
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get }
    func start()
}

final class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    let navigationController: UINavigationController

    private let window: UIWindow

    init(window: UIWindow) {
        self.window = window
        self.navigationController = UINavigationController()
    }

    func start() {
        window.rootViewController = navigationController
        window.makeKeyAndVisible()

        // Flow decision based on state
        if AuthManager.shared.isAuthenticated {
            showMainFlow()
        } else {
            showAuthFlow()
        }
    }

    private func showAuthFlow() {
        let authCoordinator = AuthCoordinator(navigationController: navigationController)
        authCoordinator.delegate = self
        childCoordinators.append(authCoordinator)
        authCoordinator.start()
    }

    private func showMainFlow() {
        let mainCoordinator = MainCoordinator(navigationController: navigationController)
        childCoordinators.append(mainCoordinator)
        mainCoordinator.start()
    }
}

pattern นี้นำลอจิกการนำทางออกจาก ViewController ทำให้เบาขึ้นและนำกลับมาใช้ใหม่ได้ง่ายขึ้น

คำถามที่ 5: จัดการการสื่อสารระหว่างโมดูลอย่างไร?

การสื่อสารระหว่างโมดูลเป็นเรื่องสำคัญในแอปขนาดใหญ่ มีหลายแนวทางขึ้นกับระดับการคัปปลิ้งที่ต้องการ

การสื่อสารผ่าน protocol

Modules/Shared/ModuleProtocols.swiftswift
// Public contracts exposed by each module
protocol PaymentModuleProtocol {
    func startPaymentFlow(for productId: String, completion: @escaping (Result<Receipt, PaymentError>) -> Void)
}

protocol UserModuleProtocol {
    func getCurrentUser() -> User?
    func updateProfile(_ profile: ProfileUpdate) async throws
}

// Modules/Payment/PaymentModule.swift
// Internal module implementation
final class PaymentModule: PaymentModuleProtocol {
    static let shared: PaymentModuleProtocol = PaymentModule()

    private let paymentService: PaymentService

    private init() {
        self.paymentService = PaymentService()
    }

    func startPaymentFlow(for productId: String, completion: @escaping (Result<Receipt, PaymentError>) -> Void) {
        // Module-internal logic
        paymentService.process(productId: productId, completion: completion)
    }
}

การสื่อสารแบบ event-driven ด้วย Combine

EventBus/AppEventBus.swiftswift
// Decoupled event bus for async communication
enum AppEvent {
    case userDidLogin(User)
    case userDidLogout
    case purchaseCompleted(Receipt)
    case subscriptionChanged(SubscriptionLevel)
}

final class AppEventBus {
    static let shared = AppEventBus()

    // Private Subject, public Publisher
    private let eventSubject = PassthroughSubject<AppEvent, Never>()
    var events: AnyPublisher<AppEvent, Never> {
        eventSubject.eraseToAnyPublisher()
    }

    func send(_ event: AppEvent) {
        eventSubject.send(event)
    }
}

// Listening in any module
final class AnalyticsModule {
    private var cancellables = Set<AnyCancellable>()

    init() {
        AppEventBus.shared.events
            .sink { [weak self] event in
                self?.handleEvent(event)
            }
            .store(in: &cancellables)
    }

    private func handleEvent(_ event: AppEvent) {
        switch event {
        case .purchaseCompleted(let receipt):
            trackPurchase(receipt)
        case .userDidLogin(let user):
            identifyUser(user)
        default:
            break
        }
    }
}
เคล็ดลับการสัมภาษณ์

ควรกล่าวถึงว่าการเลือกระหว่างคัปปลิ้งแน่น (protocol) กับคัปปลิ้งหลวม (event) ขึ้นอยู่กับบริบท: event เหมาะกับการแจ้งเตือนทั่วระบบ ส่วน protocol เหมาะกับการมีปฏิสัมพันธ์โดยตรง

คำถามที่ 6: จัดโครงสร้างการทดสอบในสถาปัตยกรรมแบบโมดูลอย่างไร?

ความสามารถในการทดสอบเป็นเกณฑ์สำคัญสำหรับตำแหน่งระดับ senior สถาปัตยกรรมที่ดีช่วยให้ทดสอบได้ง่ายในทุกระดับ

Unit test ของ ViewModel

Tests/UserViewModelTests.swiftswift
// Unit tests with injected mocks
import XCTest
@testable import MyApp

final class UserViewModelTests: XCTestCase {
    private var sut: UserViewModel!
    private var mockRepository: MockUserRepository!

    override func setUp() {
        super.setUp()
        mockRepository = MockUserRepository()
        sut = UserViewModel(userRepository: mockRepository)
    }

    override func tearDown() {
        sut = nil
        mockRepository = nil
        super.tearDown()
    }

    func test_loadUsers_success_updatesUsersArray() async {
        // Given
        let expectedUsers = [User.mock(), User.mock()]
        mockRepository.stubbedUsers = expectedUsers

        // When
        await sut.loadUsers()

        // Then
        XCTAssertEqual(sut.users.count, 2)
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.errorMessage)
    }

    func test_loadUsers_failure_setsErrorMessage() async {
        // Given
        mockRepository.stubbedError = NetworkError.noConnection

        // When
        await sut.loadUsers()

        // Then
        XCTAssertTrue(sut.users.isEmpty)
        XCTAssertNotNil(sut.errorMessage)
    }
}

// Mocks/MockUserRepository.swift
final class MockUserRepository: UserRepositoryProtocol {
    var stubbedUsers: [User] = []
    var stubbedError: Error?
    var fetchUsersCalled = false

    func fetchUsers() -> AnyPublisher<[User], Error> {
        fetchUsersCalled = true

        if let error = stubbedError {
            return Fail(error: error).eraseToAnyPublisher()
        }
        return Just(stubbedUsers)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

Integration test ของ Use Case

Tests/GetUsersUseCaseTests.swiftswift
// Integration test verifying business logic
final class GetUsersUseCaseTests: XCTestCase {
    func test_execute_sortsUsersBySubscriptionLevel() async throws {
        // Given
        let freeUser = User(id: "1", email: "free@test.com", fullName: "Free", subscriptionLevel: .free)
        let premiumUser = User(id: "2", email: "premium@test.com", fullName: "Premium", subscriptionLevel: .premium)

        let mockRepo = MockUserRepository()
        mockRepo.stubbedUsers = [freeUser, premiumUser]

        let sut = GetUsersUseCase(repository: mockRepo)

        // When
        let result = try await sut.execute()

        // Then - Premium should be first
        XCTAssertEqual(result.first?.subscriptionLevel, .premium)
        XCTAssertEqual(result.last?.subscriptionLevel, .free)
    }
}

คำถามที่ 7: จัดการสถานะ UI ที่ซับซ้อนอย่างไร?

การจัดการสถานะมีความสำคัญสูงในแอประดับ senior แนวทางที่มีโครงสร้างจะป้องกันบั๊กและทำให้การ debug ง่ายขึ้น

State machine ด้วย Enum

ViewModels/CheckoutViewModel.swiftswift
// State machine for payment flow
enum CheckoutState: Equatable {
    case idle
    case loadingCart
    case cartLoaded(CartSummary)
    case processingPayment
    case paymentSucceeded(Receipt)
    case paymentFailed(PaymentError)

    var isLoading: Bool {
        switch self {
        case .loadingCart, .processingPayment: return true
        default: return false
        }
    }
}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published private(set) var state: CheckoutState = .idle

    private let cartService: CartServiceProtocol
    private let paymentService: PaymentServiceProtocol

    init(cartService: CartServiceProtocol, paymentService: PaymentServiceProtocol) {
        self.cartService = cartService
        self.paymentService = paymentService
    }

    func loadCart() async {
        state = .loadingCart

        do {
            let summary = try await cartService.getSummary()
            state = .cartLoaded(summary)
        } catch {
            state = .paymentFailed(.cartLoadFailed)
        }
    }

    func confirmPayment() async {
        guard case .cartLoaded(let summary) = state else { return }

        state = .processingPayment

        do {
            let receipt = try await paymentService.charge(summary.total)
            state = .paymentSucceeded(receipt)
        } catch let error as PaymentError {
            state = .paymentFailed(error)
        } catch {
            state = .paymentFailed(.unknown)
        }
    }
}

แนวทางนี้ทำให้สถานะที่ขัดแย้งกันเป็นไปไม่ได้ (เช่น isLoading = true พร้อมกับแสดงข้อผิดพลาด)

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

สรุป

การสัมภาษณ์ iOS senior ประเมินความสามารถในการเลือกและให้เหตุผลกับสถาปัตยกรรมที่เหมาะกับบริบท ประเด็นสำคัญ:

Checklist สถาปัตยกรรม iOS Senior:

✅ MVVM สำหรับแอปขนาดกลางที่ใช้ SwiftUI หรือ UIKit + Combine ✅ VIPER สำหรับทีมขนาดใหญ่และโดเมนธุรกิจที่ซับซ้อน ✅ Clean Architecture เพื่อความเป็นอิสระจากเฟรมเวิร์ก ✅ Dependency Injection อย่างเป็นระบบเพื่อความสามารถในการทดสอบ ✅ Coordinator Pattern เพื่อแยกการนำทางออกจากเลเยอร์อื่น ✅ State machine สำหรับโฟลว์ที่ซับซ้อน ✅ การทดสอบในทุกระดับ: unit, integration, UI

ในการสัมภาษณ์ ต้องแสดงให้เห็น:

  • ความเข้าใจในเรื่อง trade-off (MVVM แบบเรียบง่าย vs VIPER ที่มีโครงสร้าง)
  • ประสบการณ์เชิงปฏิบัติพร้อมตัวอย่างจากโปรเจกต์จริง
  • ความสามารถในการปรับสถาปัตยกรรมให้เข้ากับบริบท (ขนาดทีม, ความซับซ้อน)
  • ความเชี่ยวชาญในการทดสอบในฐานะเกณฑ์คุณภาพของสถาปัตยกรรม

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน iOS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 29 เมษายน 2569

แท็ก

#ios
#swift
#architecture
#mvvm
#viper
#interview
#design-patterns

แชร์

บทความที่เกี่ยวข้อง

สถาปัตยกรรมการสมัครสมาชิก iOS StoreKit 2 และการตรวจสอบใบเสร็จ

การสัมภาษณ์ StoreKit 2: การจัดการการสมัครสมาชิกและการตรวจสอบใบเสร็จ

เชี่ยวชาญคำถามสัมภาษณ์ iOS เกี่ยวกับ StoreKit 2 การจัดการการสมัครสมาชิก การตรวจสอบใบเสร็จ และการนำการซื้อในแอปไปใช้ พร้อมตัวอย่างโค้ด Swift ที่ใช้งานได้จริง

Swift Testing Framework พร้อมมาโคร #expect และ #require สำหรับการสัมภาษณ์ iOS

Swift Testing Framework สัมภาษณ์ 2026: มาโคร #expect และ #require เทียบกับ XCTest

เรียนรู้ Swift Testing Framework ใหม่สำหรับการสัมภาษณ์ iOS: มาโคร #expect และ #require การย้ายจาก XCTest แพทเทิร์นขั้นสูงและข้อผิดพลาดที่พบบ่อย

สถาปัตยกรรม APNs ของ iOS Push Notifications พร้อมโทเคนและ troubleshooting

สัมภาษณ์ iOS Push Notifications 2026: APNs, โทเคน และ troubleshooting

คู่มือเตรียมสัมภาษณ์ iOS อย่างครบถ้วนเกี่ยวกับ Push Notifications, APNs, การจัดการโทเคน และ troubleshooting พร้อมคำถามยอดนิยมและคำตอบโดยละเอียด