NSUserDefaults — คืออะไร API UserDefaults และการทำงานกับการตั้งค่า iOS

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-12 เวลาอ่าน: 10 นาที

NSUserDefaults เป็นที่เก็บข้อมูลแบบคีย์-ค่าบน iOS, watchOS, tvOS และ macOS ที่ออกแบบมาเพื่อบันทึกการตั้งค่าและการกำหนดค่าของแอปพลิเคชัน ข้อมูลจะถูกเก็บไว้ในไฟล์ plist ในแซนด์บ็อกซ์ของแอปพลิเคชันและซิงค์กับ iCloud โดยอัตโนมัติผ่าน NSUbiquitousKeyValueStore ตามเอกสารทางการจาก Apple Developer, 2025 NSUserDefaults รองรับการเก็บชนิดข้อมูลพื้นฐาน: String, Int, Bool, Float, Double, Data, Date, Array และ Dictionary คลาสนี้ถูกเปลี่ยนชื่อเป็น UserDefaults ตั้งแต่ Swift 3 แต่ชื่อ Objective-C คือ NSUserDefaults ยังคงใช้กันอย่างแพร่หลายในฐานโค้ดและเอกสารของ Apple

ประเด็นสำคัญ

  • NSUserDefaults เป็นที่เก็บแบบคีย์-ค่าบน iOS และ macOS สำหรับบันทึกการตั้งค่าแอปพลิเคชันอย่างง่ายในไฟล์ plist
  • รองรับชนิดข้อมูลเก้าชนิด: String, Int, Bool, Float, Double, Data, Date, Array และ Dictionary
  • ข้อมูลซิงค์กับ iCloud โดยอัตโนมัติผ่าน NSUbiquitousKeyValueStore โดยการสนับสนุนของนักพัฒนา
  • ใช้ระบบ โดเมน ที่มีการค้นหาค่าแบบเรียงลำดับตามห่วงโซ่โดเมน
  • สำหรับเก็บข้อมูลที่ละเอียดอ่อน Apple แนะนำให้ใช้ Keychain แทน NSUserDefaults

NSUserDefaults คืออะไร?

NSUserDefaults (UserDefaults ใน Swift) เป็นกลไกในตัวของ Apple สำหรับเก็บคู่คีย์-ค่าในรูปแบบ plist โดยมีให้ใช้งานบนทุกแพลตฟอร์มของ Apple: iOS, iPadOS, watchOS, tvOS และ macOS วัตถุประสงค์หลักคือการบันทึกค่ากำหนดของผู้ใช้ สถานะอินเทอร์เฟซ แฟลกการเปิดครั้งแรก ตัวเลือกที่เลือก และข้อมูลง่ายอื่น ๆ ที่คงอยู่ข้ามการรีสตาร์ทแอปพลิเคชัน

แต่ละแอปพลิเคชัน iOS มีแซนด์บ็อกซ์ที่แยกออกจากกัน และ NSUserDefaults จะถูกเก็บไว้ในไดเรกทอรี Library/Preferences ภายในแซนด์บ็อกซ์นี้ในไฟล์ที่ชื่อตาม Bundle Identifier ไฟล์ plist ประกอบด้วยคู่คีย์-ค่า โดยที่คีย์เป็นสตริงและค่าเป็นหนึ่งในชนิดที่รองรับ ขนาดไฟล์ไม่ถูกจำกัด แต่ Apple แนะนำให้เก็บเฉพาะการตั้งค่าใน UserDefaults ไม่ใช่ข้อมูลปริมาณมาก

ตั้งแต่ iOS 8 เป็นต้นมา NSUserDefaults เริ่มรองรับ App Groups — ที่เก็บข้อมูลร่วมกันระหว่างแอปพลิเคชันของนักพัฒนาคนเดียวกันและส่วนขยายของพวกเขา (วิดเจ็ต, แอปพลิเคชันคู่หู watchOS) โดยใช้ตัวเริ่มต้น init?(suiteName:) พร้อมกับตัวระบุ App Group ซึ่งช่วยให้วิดเจ็ต Today สามารถอ่านการตั้งค่าจากแอปพลิเคชันหลักได้โดยไม่ต้องทำซ้ำตรรกะการบันทึก

รูปแบบการจัดเก็บ: plist บนอุปกรณ์

ทางกายภาพ NSUserDefaults จะถูกเก็บไว้ในไฟล์ plist แบบไบนารีที่: {Sandbox}/Library/Preferences/com.example.myapp.plist ไฟล์ใช้รูปแบบ plist แบบไบนารี (NSPropertyListBinaryFormat_v1_0) เพื่อความกะทัดรัดและความเร็วในการอ่าน บน macOS ไฟล์อาจอยู่ในรูปแบบ XML เพื่อความเข้ากันได้ แตกต่างจาก SharedPreferences บน Android ไฟล์ plist ของ UserDefaults สามารถมีโครงสร้างซ้อนกันผ่าน Dictionary และ Array

ไฟล์ NSUserDefaults ไม่ได้ถูกเข้ารหัสตามค่าเริ่มต้น ข้อมูลถูกเก็บในรูปแบบข้อความธรรมดาและสามารถอ่านได้เมื่อมีการเข้าถึงอุปกรณ์ทางกายภาพหรือผ่านการสำรองข้อมูล สำหรับการเก็บข้อมูลที่ละเอียดอ่อน (รหัสผ่าน โทเคน คีย์เข้ารหัส) Apple แนะนำอย่างยิ่งให้ใช้ Keychain ซึ่งเข้ารหัสข้อมูลโดยอัตโนมัติในระดับระบบปฏิบัติการ

NSUserDefaults ทำงานอย่างไรใน iOS

NSUserDefaults ทำงานบนหลักการแคชในหน่วยความจำพร้อมการซิงค์ดิสก์เป็นระยะ เมื่อเข้าถึงอินสแตนซ์มาตรฐาน UserDefaults.standard ครั้งแรก ระบบจะโหลดไฟล์ plist เข้าสู่ RAM ในรูปแบบ Dictionary การอ่านทั้งหมดที่ตามมาจะดำเนินการจากหน่วยความจำ การเขียนก็เกิดขึ้นในหน่วยความจำก่อนเช่นกัน โดยการซิงค์ดิสก์จะเกิดขึ้นเป็นระยะในเธรดพื้นหลัง

การดำเนินการเขียนใช้เมธอด set(_:forKey:) ซึ่งรับค่าแบบตัวเลือกชนิด Any? ค่าสามารถเป็น nil — ใช้เพื่อลบคีย์ สำหรับการเขียนลงดิสก์ทันที เดิมทีใช้เมธอด synchronize() แต่ตั้งแต่ iOS 7 และ OS X 10.9 เป็นต้นมาไม่จำเป็นอีกต่อไป — ระบบจะซิงค์ข้อมูลโดยอัตโนมัติในช่วงเวลาปกติ Apple ประกาศอย่างเป็นทางการว่า synchronize() นั้นซ้ำซ้อนในเอกสารของบริษัท

รีจิสเตอร์ของ UserDefaults และโดเมน

NSUserDefaults ใช้ระบบรีจิสเตอร์ (โดเมน) เพื่อจัดระเบียบการค้นหาค่า เมื่อแอปพลิเคชันร้องขอค่าตามคีย์ UserDefaults จะตรวจสอบโดเมนตามลำดับในลำดับที่กำหนด: อันดับแรก NSArgumentDomain (อาร์กิวเมนต์บรรทัดคำสั่ง) จากนั้นโดเมนแอปพลิเคชัน จากนั้น NSGlobalDomain (การตั้งค่าระบบ) จากนั้นโดเมนเฉพาะภาษา และสุดท้าย NSRegistrationDomain (ค่าที่ลงทะเบียนผ่าน register(defaults:))

swift
import Foundation

// อินสแตนซ์มาตรฐานของ UserDefaults
let defaults = UserDefaults.standard

// การเขียนค่า
defaults.set("อันนา เปโตรวา", forKey: "username")
defaults.set(28, forKey: "age")
defaults.set(true, forKey: "isLoggedIn")

// การลงทะเบียนค่าเริ่มต้น
defaults.register(defaults: [
    "theme": "system",
    "fontSize": 14
])

// การอ่านพร้อมส่งคืนค่าเริ่มต้น
let theme = defaults.string(forKey: "theme") ?? "system"
let fontSize = defaults.integer(forKey: "fontSize")

โดเมน NSRegistrationDomain เป็นโดเมนเชิงโปรแกรมที่มีอยู่ในหน่วยความจำเท่านั้นและไม่ถูกจัดเก็บลงดิสก์ ใช้สำหรับกำหนดค่าเริ่มต้นซึ่งจะมีผลจนกว่าแอปพลิเคชันจะเขียนค่าของตัวเองลงในโดเมนแอปพลิเคชัน ซึ่งช่วยให้สามารถสร้างจุดกำหนดค่าเดียวสำหรับการตั้งค่าเริ่มต้นที่สามารถเปลี่ยนแปลงได้จากส่วนกลางในระหว่างการพัฒนา

วิธีการหลักของ NSUserDefaults

NSUserDefaults มีชุดเมธอดแบบกำหนดชนิดสำหรับการอ่านและเขียนข้อมูล: string(forKey:), integer(forKey:), bool(forKey:), float(forKey:), double(forKey:), data(forKey:), array(forKey:), dictionary(forKey:) และ object(forKey:) เมธอดอ่านแต่ละเมธอดมีเมธอดเขียนที่สอดคล้องกัน set(_:forKey:) พร้อมการกำหนดชนิดของค่าที่เก็บโดยอัตโนมัติ รุ่น Swift ของ UserDefaults ใช้การกำหนดชนิดที่เข้มงวด ในขณะที่รุ่น Objective-C รับและส่งคืน id

เมธอดอ่าน (Swift)ชนิดข้อมูลค่าเริ่มต้น
string(forKey:)String?nil
integer(forKey:)Int0
bool(forKey:)Boolfalse
float(forKey:)Float0.0
double(forKey:)Double0.0
data(forKey:)Data?nil

synchronize และความเกี่ยวข้อง

เมธอด synchronize() ใน NSUserDefaults บังคับเขียนการเปลี่ยนแปลงทั้งหมดจากหน่วยความจำลงดิสก์ ในเวอร์ชันแรก ๆ ของ iOS เมธอดนี้ต้องถูกเรียกหลังจากทุกการเขียนเพื่อรับประกันความคงอยู่ของข้อมูล ตั้งแต่ iOS 7 เป็นต้นมา ระบบซิงค์ UserDefaults โดยอัตโนมัติในเธรดพื้นหลัง และ Apple ประกาศอย่างเป็นทางการว่า synchronize() นั้นซ้ำซ้อน การเรียกเมธอดนี้ไม่ทำให้เกิดข้อผิดพลาด แต่ไม่ได้ให้การรับประกันความคงอยู่เพิ่มเติม

สำหรับการติดตามการเปลี่ยนแปลง NSUserDefaults มีการแจ้งเตือน UserDefaults.didChangeNotification และเมธอดการสังเกต KVO addObserver(_:forKeyPath:options:context:) ใน SwiftUI มี Property Wrapper @AppStorage ซึ่งซิงค์ค่าใน UserDefaults กับการอัปเดตอินเทอร์เฟซผู้ใช้โดยอัตโนมัติ @AppStorage รองรับชนิดเดียวกันกับ UserDefaults และเป็นวิธีที่ต้องการสำหรับการทำงานกับการตั้งค่าในแอปพลิเคชัน SwiftUI

swift
// การสังเกตการเปลี่ยนแปลงผ่าน KVO
class SettingsViewModel: NSObject {
    override func observeValue(
        forKeyPath keyPath: String?,
        of object: Any?,
        change: [NSKeyValueChangeKey: Any]?,
        context: UnsafeMutableRawPointer?
    ) {
        guard let keyPath else { return }
        print("คีย์เปลี่ยนแล้ว: \(keyPath)")
    }
}

// SwiftUI - AppStorage
struct SettingsView: View {
    @AppStorage("theme") private var theme: String = "system"

    var body: some View {
        Picker("ธีม", selection: $theme) {
            Text("ระบบ").tag("system")
            Text("สว่าง").tag("light")
            Text("มืด").tag("dark")
        }
    }
}

สำหรับการทำงานกับ App Groups (ที่เก็บข้อมูลร่วมกันระหว่างแอปพลิเคชันและส่วนขยายของมัน) ให้ใช้ตัวเริ่มต้น UserDefaults(suiteName:) พร้อมกับตัวระบุ App Group ตัวอย่างเช่น “group.com.example.myapp” ข้อมูลที่เขียนลงในอินสแตนซ์นี้สามารถเข้าถึงได้จากแอปพลิเคชันหลัก วิดเจ็ต แอปพลิเคชันคู่หู watchOS และส่วนขยายอื่น ๆ ที่เป็นของ App Group เดียวกัน แต่ละอินสแตนซ์ suite จะถูกเก็บในไฟล์ plist แยกต่างหาก

NSUserDefaults กับทางเลือกการจัดเก็บอื่น

แม้จะสะดวกและเรียบง่าย แต่ NSUserDefaults ไม่ใช่โซลูชันการจัดเก็บแบบสากลสำหรับข้อมูลทุกชนิดบน iOS ขึ้นอยู่กับปริมาณ ความสำคัญ และข้อกำหนดด้านความปลอดภัย Apple มีทางเลือกหลายอย่าง ซึ่งแต่ละอย่างถูกปรับให้เหมาะสมกับกรณีการใช้งานเฉพาะ

โซลูชันเมื่อใดควรใช้ข้อจำกัด
NSUserDefaultsการตั้งค่าอินเทอร์เฟซและการกำหนดค่าไม่เหมาะสำหรับข้อมูลขนาดใหญ่หรือความลับ
Keychainรหัสผ่าน โทเคน คีย์เข้ารหัสซับซ้อนกว่าในการใช้งาน ช้ากว่า
CoreDataข้อมูลที่มีโครงสร้างพร้อมความสัมพันธ์มากเกินไปสำหรับ 10–20 การตั้งค่า
FileManagerเอกสาร รูปภาพ ข้อมูลไบนารีต้องจัดการไฟล์ด้วยตนเอง
CloudKitซิงค์คลาวด์ระหว่างอุปกรณ์ต้องมีบัญชี iCloud และการเชื่อมต่อเครือข่าย

Keychain — ที่เก็บข้อมูลที่ปลอดภัย

Keychain คือที่เก็บข้อมูลที่ปลอดภัยของ Apple สำหรับข้อมูลที่เป็นความลับ แตกต่างจาก NSUserDefaults ข้อมูลทั้งหมดใน Keychain จะถูกเข้ารหัสในระดับระบบปฏิบัติการโดยใช้การเข้ารหัสฮาร์ดแวร์จาก Secure Enclave บนอุปกรณ์ที่รองรับ Keychain จะล็อกและปลดล็อกโดยอัตโนมัติพร้อมกับอุปกรณ์และรองรับการแบ่งปันการเข้าถึงระหว่างแอปพลิเคชันของนักพัฒนาคนเดียวกันผ่าน Keychain Access Groups

ข้อเสียหลักของ Keychain คือความซับซ้อนของ API เพียงแค่บันทึกสตริงธรรมดา คุณต้องสร้างคำสั่ง SecItemAdd โดยระบุแอตทริบิวต์: คลาส (kSecClassGenericPassword), บริการ (kSecAttrService), บัญชี (kSecAttrAccount) และข้อมูลจริง (kSecValueData) เพื่อให้การทำงานกับ Keychain ง่ายขึ้น มี wrapper ของบริษัทอื่นเช่น KeychainAccess และ SwiftKeychainWrapper ซึ่งให้อินเทอร์เฟซคีย์-ค่าที่สะดวกคล้ายกับ UserDefaults

ตัวอย่างการใช้ NSUserDefaults ใน Swift

ลองพิจารณาตัวอย่างที่ใช้งานได้จริง: การบันทึกและกู้คืนสถานะการแนะนำ (หน้าต้อนรับ) ในแอปพลิเคชัน iOS โดยใช้ NSUserDefaults เมื่อเปิดครั้งแรก ผู้ใช้จะเห็นหน้าจอแนะนำ หลังจากทำครบแล้ว แฟลกจะถูกบันทึกใน UserDefaults ในการเปิดครั้งถัดไป จะข้ามการแนะนำไป สำหรับ SwiftUI ใช้ @AppStorage; สำหรับ UIKit ใช้การเข้าถึง UserDefaults.standard โดยตรง

การบันทึกสถานะการแนะนำ

มาสร้าง OnboardingManager ที่ห่อหุ้มการทำงานกับ UserDefaults สำหรับเก็บสถานะการแนะนำ ผู้จัดการมีคุณสมบัติ isOnboardingCompleted สำหรับตรวจสอบสถานะและเมธอด markOnboardingCompleted สำหรับตั้งค่าแฟลก คีย์จัดเก็บถูกแยกเป็นค่าคงที่เพื่อป้องกันการพิมพ์ผิด สำหรับการทดสอบหน่วย ผู้จัดการใช้โปรโตคอล UserDefaultsProtocol ซึ่งช่วยให้สามารถแทนที่ที่เก็บจริงด้วย MockUserDefaults

swift
class OnboardingManager {
    private let defaults: UserDefaults
    private let hasSeenKey = "has_seen_onboarding"

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    var isOnboardingCompleted: Bool {
        defaults.bool(forKey: hasSeenKey)
    }

    func markOnboardingCompleted() {
        defaults.set(true, forKey: hasSeenKey)
    }

    func resetOnboarding() {
        defaults.removeObject(forKey: hasSeenKey)
    }
}

// การใช้งานในแอปพลิเคชัน
let onboardingManager = OnboardingManager()
if !onboardingManager.isOnboardingCompleted {
    showOnboarding()
} else {
    showMainScreen()
}

สำหรับการเก็บการตั้งค่าที่ซับซ้อนมากขึ้น เช่น ออบเจกต์ Profile ที่มีโครงสร้าง แนะนำให้ใช้โปรโตคอล Codable และ JSONEncoder/JSONDecoder ออบเจกต์ถูกทำให้เป็นอนุกรมเป็น Data ผ่าน JSONEncoder บันทึกผ่าน set(_:forKey:) และเมื่ออ่านจะถูกดีซีเรียลไลซ์จาก Data กลับเป็นออบเจกต์ผ่าน JSONDecoder วิธีการนี้ช่วยให้สามารถเก็บโครงสร้างที่ซับซ้อนใน UserDefaults โดยไม่สูญเสียความปลอดภัยของชนิด

swift
struct UserProfile: Codable {
    let name: String
    let age: Int
    let preferences: [String: String]
}

extension UserDefaults {
    func save<T: Codable>(_ value: T, forKey key: String) {
        if let data = try? JSONEncoder().encode(value) {
            set(data, forKey: key)
        }
    }

    func load<T: Codable>(_ type: T.Type, forKey key: String) -> T? {
        guard let data = data(forKey: key) else { return nil }
        return try? JSONDecoder().decode(type, from: data)
    }
}

// การใช้งาน
let profile = UserProfile(name: "อันนา", age: 28, preferences: ["theme": "dark"])
UserDefaults.standard.save(profile, forKey: "user_profile")
let loaded = UserDefaults.standard.load(UserProfile.self, forKey: "user_profile")

สิ่งสำคัญที่ต้องจำคือ NSUserDefaults ไม่ได้ออกแบบมาสำหรับเก็บข้อมูลปริมาณมาก Apple แนะนำให้จำกัดข้อมูลที่เก็บไว้ไม่เกินสองสามสิบกิโลไบต์ สำหรับการเก็บออบเจกต์ขนาดใหญ่ (รูปภาพ เอกสาร โมเดลที่ทำให้เป็นอนุกรม) ให้ใช้ FileManager กับไดเรกทอรี Documents หรือ CoreData นอกจากนี้ UserDefaults ไม่รองรับการกำหนดเวอร์ชันสคีมาข้อมูล — เมื่อโครงสร้างโมเดล Codable เปลี่ยนแปลง ข้อมูลเก่าอาจดีซีเรียลไลซ์ไม่ได้ และสิ่งนี้ต้องได้รับการจัดการในโค้ดของแอปพลิเคชัน

คำถามที่พบบ่อย

NSUserDefaults แตกต่างจาก SharedPreferences บน Android อย่างไร?

ทั้งคู่เป็นที่เก็บคีย์-ค่า แต่ NSUserDefaults รองรับชนิดมากกว่า (Data, Date, Array, Dictionary) และซิงค์กับ iCloud โดยอัตโนมัติ SharedPreferences เก็บข้อมูลใน XML NSUserDefaults เก็บในรูปแบบ plist NSUserDefaults มีระบบโดเมนพร้อมการค้นหาแบบเรียงลำดับ ในขณะที่ SharedPreferences ใช้โครงสร้างแบบแบนง่ายพร้อมชื่อไฟล์

การเก็บรหัสผ่านใน NSUserDefaults ปลอดภัยหรือไม่?

ไม่ NSUserDefaults เก็บข้อมูลในรูปแบบข้อความธรรมดาโดยไม่มีการเข้ารหัส สำหรับรหัสผ่าน โทเคน และคีย์เข้ารหัส ให้ใช้ Keychain ซึ่งเข้ารหัสข้อมูลในระดับ Secure Enclave Keychain ยังรองรับแอตทริบิวต์การเข้าถึงเช่นการยืนยันตัวตนด้วยไบโอเมตริก (Face ID / Touch ID) ก่อนอ่านความลับ

จะซิงค์ NSUserDefaults ระหว่างอุปกรณ์ได้อย่างไร?

สำหรับการซิงค์ระหว่างอุปกรณ์ของผู้ใช้คนเดียวกัน ให้ใช้ NSUbiquitousKeyValueStore — ที่เก็บคีย์-ค่าบนคลาวด์ iCloud ข้อมูลที่เขียนในบริการนี้บนอุปกรณ์หนึ่งจะปรากฏบนอุปกรณ์อื่นทั้งหมดที่มีบัญชี iCloud เดียวกันโดยอัตโนมัติ ความจุสูงสุด: 1 MB ต่อแอปพลิเคชัน, 1024 คีย์

จะลบข้อมูลทั้งหมดจาก NSUserDefaults ได้อย่างไร?

ในการลบข้อมูลทั้งหมด ให้เรียกเมธอด removePersistentDomain(forName:) พร้อม Bundle Identifier ของแอปพลิเคชัน ในการลบค่าแต่ละค่า ให้ใช้ removeObject(forKey:) สำหรับการรีเซ็ตการตั้งค่าทั้งหมด: UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) การลบทั้งหมดจะถูกนำไปใช้กับแคชในหน่วยความจำทันที

ขนาดข้อมูลสูงสุดใน NSUserDefaults คือเท่าใด?

Apple ไม่ได้กำหนดขีดจำกัดที่เข้มงวดสำหรับขนาดของ NSUserDefaults แต่แนะนำไม่ให้เกิน 100 KB ของปริมาณรวมของข้อมูลที่เก็บทั้งหมด สำหรับปริมาณที่มากขึ้น ให้ใช้ CoreData หรือ FileManager เมื่อเก็บข้อมูลมากกว่า 1 MB ประสิทธิภาพการอ่านเมื่อเริ่มต้นแอปพลิเคชันอาจลดลงอย่างเห็นได้ชัด

สรุป

  • NSUserDefaults คือที่เก็บคีย์-ค่าในตัวของ Apple สำหรับการตั้งค่าแอปพลิเคชันอย่างง่ายในรูปแบบ plist
  • รองรับชนิดข้อมูลเก้าชนิด: String, Int, Bool, Float, Double, Data, Date, Array และ Dictionary
  • ใช้ระบบโดเมนที่มี การค้นหาแบบเรียงลำดับ ผ่าน NSRegistrationDomain, NSGlobalDomain และโดเมนแอปพลิเคชัน
  • สำหรับการซิงค์ระหว่างอุปกรณ์ Apple ให้นำเสนอ NSUbiquitousKeyValueStore โดยมีขีดจำกัด 1 MB ต่อแอปพลิเคชัน
  • เพื่อความปลอดภัย ให้ใช้ Keychain สำหรับรหัสผ่านและโทเคน ไม่ใช่ NSUserDefaults
  • ใน SwiftUI วิธีที่ต้องการคือ Property Wrapper @AppStorage พร้อมการซิงค์อินเทอร์เฟซอัตโนมัติ
  • สำหรับข้อมูลขนาดใหญ่หรือมีโครงสร้าง ให้เลือก CoreData หรือ FileManager แทน NSUserDefaults

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม