แอปที่ใช้งานได้ปกติทุกวัน อาจกลายเป็นแอปที่แก้ไม่ได้โดยไม่มีใครทันสังเกต ไม่ใช่เพราะโค้ดพัง แต่เพราะมือถือ ระบบปฏิบัติการ และเครื่องมือที่ใช้สร้างแอปเดินหน้าไปทุกปี ขณะที่แอปหยุดอยู่ที่เดิม บทความนี้เล่างานอัปเกรด React Native จริงที่เราทำให้ลูกค้ารายหนึ่ง เป็นแอปใช้ภายในองค์กรสำหรับช่างภาคสนามของบริษัทจัดจำหน่ายเครื่องดื่ม ที่ค้างอยู่บน React Native 0.64 มาตั้งแต่ปี 2021 และต้องขึ้นไปที่ 0.82 ว่าต้องเปลี่ยนอะไร เจออะไร และทดสอบยังไงก่อนปล่อยจริง
ทำไมแอปที่ยังใช้ได้ถึงต้องอัปเกรด
แอปในงานนี้ไม่ได้อยู่บน Play Store แต่แจกจ่ายใช้กันภายในองค์กร จึงไม่มีเส้นตายของ Google Play มาบังคับ ที่ต้องอัปเกรดเพราะลูกค้าเจอปัญหาสามข้อพร้อมกัน
- build แอปไม่ผ่าน Gradle 6.7 กับ Android Gradle Plugin 4.1 เก่าเกินกว่าจะใช้กับ JDK และ Android SDK ปัจจุบัน แค่จะแก้บั๊กบรรทัดเดียวก็สร้างไฟล์ติดตั้งใหม่ไม่ได้แล้ว
- library เริ่มมีปัญหา หลายตัวเลิกพัฒนาไปแล้ว เช่น react-native-camera และ rn-fetch-blob ไม่มีใครออกเวอร์ชันแก้ให้อีก
- ใช้กับ Android รุ่นใหม่ไม่ได้ Android 13 ขึ้นไปเปลี่ยนวิธีขอสิทธิ์เข้าถึงไฟล์ กล้อง และตำแหน่ง ฟีเจอร์อย่างแนบไฟล์และบันทึกรูปลายเซ็นจึงทำงานไม่ได้บนมือถือเครื่องใหม่ ๆ ที่ช่างใช้
ทั้งสามข้อมีต้นเหตุเดียวกัน คือแอปหยุดอยู่ที่ปี 2021 ขณะที่ทุกอย่างรอบตัวเดินหน้าไปสี่ปี การแก้ทีละอาการจึงไม่พอ ต้องยกทั้งแอปขึ้นมา
ถ้าแอปของคุณอยู่บน Play Store จะมีเส้นตายเพิ่มอีกชั้น ตั้งแต่ 31 สิงหาคม 2026 แอปใหม่และทุกการอัปเดตต้อง target Android 16 (API 36) ขึ้นไป ส่วนแอปเดิมที่ target ต่ำกว่า Android 15 (API 35) จะไม่แสดงให้ผู้ใช้ใหม่ที่ใช้ Android รุ่นใหม่กว่านั้นเห็น ทั้งสองกรณี React Native 0.64 และ Gradle รุ่นที่แอปนี้ใช้อยู่ขยับขึ้นไปถึง API 36 ไม่ได้ การเปลี่ยนตัวเลขในไฟล์ตั้งค่าบรรทัดเดียวจึงไม่พอ ต้องยกทั้งชุดเครื่องมือขึ้นมาพร้อมกัน
สภาพแอปก่อนและหลังอัปเกรด
- React Native 0.64.4 และ React 17 (ออกปี 2021) เป็น React Native 0.82.1 และ React 19.1
- target API 29 (Android 10) เป็น API 36 (Android 16) ตรงกับ Android รุ่นล่าสุด
- Gradle 6.7 กับ Android Gradle Plugin 4.1 เป็น Gradle 9 กับ Kotlin 2.1 บน JDK 17
- JavaScriptCore เป็น Hermes และรันบน New Architecture ซึ่ง 0.82 บังคับใช้
- dependency 105 รายการที่หลายตัวเลิกพัฒนาแล้ว เหลือราว 60 รายการที่ยังมีคนดูแล
- react-native-router-flux ปนกับ React Navigation v5 เป็น React Navigation ชุดเดียวทั้งแอป
สิ่งที่ไม่ได้เปลี่ยนคือหน้าตาและ flow การทำงาน แอปมี 37 โมดูล ตั้งแต่ใบสั่งงาน เบิกอะไหล่ โอนเครื่องมือ checklist ของผู้ตรวจ ลายเซ็นลูกค้า สแกน QR ไปจนถึงแผนที่ ช่างใช้แอปเหมือนเดิมทุกหน้า งานนี้คือการเปลี่ยนฐานข้างใต้ ไม่ใช่การทำแอปใหม่
ทำไมเริ่มจากโปรเจกต์ใหม่ แทนการไล่อัปทีละเวอร์ชัน
วิธีมาตรฐานคือไล่อัปทีละเวอร์ชันตามคู่มือ upgrade แต่จาก 0.64 ถึง 0.82 คือสิบแปดเวอร์ชัน แต่ละรอบมีไฟล์ฝั่ง Android ที่ต้องแก้ตาม และต้อง build ผ่านทุกรอบก่อนไปต่อ ซึ่งกับ library ที่เลิกพัฒนาไปหลายตัว แทบเป็นไปไม่ได้ เราจึงสร้างโปรเจกต์ 0.82.1 ขึ้นใหม่ ตั้งค่าฝั่ง Android ตามมาตรฐานปัจจุบันตั้งแต่ต้น แล้วย้ายโค้ดเดิมกว่า 300 ไฟล์เข้ามาทำให้ทำงานทีละโมดูล โครงฝั่ง native จึงสะอาดเหมือนแอปใหม่ ขณะที่ business logic ยังเป็นของเดิมที่ผ่านการใช้งานจริงมาหลายปี
คัดแยก library ทีละตัว
งานที่กินเวลาที่สุดไม่ใช่ตัว React Native แต่คือ library รอบตัว ทุกตัวต้องถูกจัดเข้ากลุ่มใดกลุ่มหนึ่งในสี่กลุ่มนี้
- อัปเวอร์ชันได้ตรง ๆ เช่น react-native-reanimated จาก 2 เป็น 4, lottie-react-native จาก 4 เป็น 7 และ react-native-vector-icons จาก 8 เป็น 10 แต่ต้องตามแก้โค้ดที่เรียกใช้ตาม breaking change ของแต่ละตัว
- เปลี่ยนไปใช้ตัวที่ยังมีคนดูแล เช่น react-native-camera เป็น react-native-vision-camera, rn-fetch-blob เป็น react-native-blob-util, react-native-image-resizer เป็นของ @bam.tech และ react-native-exif เป็น @lodev09/react-native-exify
- ยังไม่มีตัวแทนที่ดีพอ จึงแก้โค้ดของ library เองด้วย patch-package ในงานนี้มีห้าตัว ได้แก่ ปุ่มลอย ตัวสแกน QR ช่องเซ็นลายมือ รายการแบบปัดซ้ายขวา และตัวจัดการไฟล์
- ไม่ได้ใช้แล้วจริง ถอดออก ซึ่งเป็นเหตุผลที่ dependency ลดจาก 105 เหลือราว 60
ตัวที่ใหญ่ที่สุดคือระบบเปลี่ยนหน้าจอ
react-native-router-flux ไม่มีการพัฒนาต่อมาหลายปีและไม่รองรับ React Native รุ่นใหม่ จึงต้องย้ายไป React Navigation ทั้งแอป ทุกจุดที่สั่ง Actions.ชื่อหน้า() กลายเป็น navigation.navigate() และทุกหน้าที่รับค่าผ่าน props ต้องไปอ่านจาก route.params ส่วนนี้อันตรายกว่าที่เห็น เพราะถ้าลืมส่งค่า แอปจะไม่ crash แต่หน้านั้นจะเปิดขึ้นมาแบบไม่มีข้อมูลเงียบ ๆ เช่น เปิดหน้าเพิ่มอะไหล่แล้วไม่รู้ว่าเป็นของใบสั่งงานไหน
สิ่งที่พังเพราะ Android และ React Native รุ่นใหม่
- New Architecture ถูกบังคับใช้ตั้งแต่ 0.82 ปิดไม่ได้อีกแล้ว ส่วน PropTypes ถูกถอดออกจาก core library ที่ยังเขียนแบบเก่าจึงต้องพึ่ง interop layer หรือต้อง patch
- แอปที่ target API 35 ขึ้นไปถูกบังคับให้แสดงผลแบบ edge-to-edge เต็มจอถึงขอบ และ React Native 0.81 ขึ้นไปเปิดเป็นค่าเริ่มต้น หน้าจอที่ไม่ได้เผื่อพื้นที่ไว้จะมีเนื้อหาไปซ้อนใต้แถบสถานะ ต้องเปลี่ยนมาใช้ SafeAreaView จาก react-native-safe-area-context ทุกหน้า
- สิทธิ์เข้าถึงไฟล์ กล้อง และตำแหน่งบน Android 13 ขึ้นไปเปลี่ยนวิธีขอ ฟีเจอร์ถ่ายรูปงาน แนบไฟล์ และเช็กอินด้วย GPS ต้องปรับตาม
ทดสอบยังไงเมื่อแอปไม่มีเทสต์อัตโนมัติ
แอปเดิมแทบไม่มีเทสต์อัตโนมัติ นอกจากไฟล์ตัวอย่างที่ติดมากับ template ซึ่งพบบ่อยในแอปองค์กรที่อายุหลายปี และการเขียนเทสต์ครอบทั้งแอปย้อนหลังเป็นงานที่ใหญ่กว่าตัวอัปเกรดเอง งานรอบนี้จึงพึ่งการทดสอบด้วยมือแบบมีรายการชัดเจนสองชั้น
ชั้นที่ 1 ทีมเราทดสอบบนมือถือจริง
ฟีเจอร์ที่พังง่ายที่สุดหลังอัปเกรดคือฟีเจอร์ที่คุยกับฮาร์ดแวร์ของเครื่อง ซึ่ง emulator จำลองได้ไม่ครบ จึงไล่ทดสอบบนมือถือ Android จริงหลายรุ่นหลายเวอร์ชัน เน้นกล้องและสแกน QR ถ่ายรูปงานพร้อมข้อมูล EXIF ช่องเซ็นลายมือ GPS กับแผนที่ ไฟล์ PDF และการแจ้งเตือนแบบ push แล้วไล่ทุก flow ที่ส่งค่าข้ามหน้าจอ เช่น จากใบสั่งงานไปหน้าเพิ่มอะไหล่และหน้าเซ็นปิดงาน
ชั้นที่ 2 UAT กับผู้ใช้จริงของลูกค้า
หลังทีมเราทดสอบผ่าน ผู้ใช้จริงฝั่งลูกค้าลองใช้แอปกับงานประจำวันก่อนแจกจ่ายให้ช่างทุกคน ขั้นนี้จับสิ่งที่รายการทดสอบของเราไม่มีทางรู้ได้ เช่น ลำดับการกดที่ช่างใช้จริงหน้างาน หรือข้อมูลบางรูปแบบที่มีเฉพาะในระบบจริง
ส่งมอบอะไรบ้าง นอกจากตัวแอป
การอัปเกรดที่ไม่มีเอกสาร ทำให้ทีมถัดไปต้องเริ่มสืบใหม่จากศูนย์ในรอบหน้า เราจึงส่งมอบเอกสารคู่กับโค้ด
- ตารางเทียบ library เดิมกับใหม่ พร้อมเหตุผลที่เปลี่ยนแต่ละตัว
- คู่มือ navigation แบบใหม่ เทียบคำสั่งเดิมทีละแบบ และรายชื่อหน้าจอที่รับค่าผ่าน route params ไว้ไล่ปัญหาข้อมูลไม่ขึ้น
- เหตุผลของ patch ทั้งห้าตัว วิธีสร้าง patch ใหม่ และเมื่อไหร่ควรลบทิ้ง
- ขั้นตอน build ไฟล์ติดตั้ง APK และ App Bundle พร้อมคำสั่งลัดใน VS Code
อัปเกรดปีละครั้ง ถูกกว่าอัปเกรดสี่ปีครั้ง
งานนี้ใช้เวลาราวสองเดือนครึ่ง เหตุผลหลักคือกระโดดข้ามสี่ปีในรอบเดียว library ที่ต้องเปลี่ยนตัว ระบบ navigation ที่ต้องรื้อ และเครื่องมือ build ที่ต้องยกทั้งชุด ล้วนเป็นผลของการรอนาน ถ้าแอปขยับทุกปีตามรอบ Android รุ่นใหม่ (แอปบน Play Store คือให้เสร็จก่อนเส้นตายปลายเดือนสิงหาคม) แต่ละรอบจะเป็นงานไม่กี่สัปดาห์ที่รวมไว้ในสัญญาดูแลระบบได้ และไม่ต้องเจอวันที่แก้บั๊กด่วนไม่ได้เพราะ build ไม่ผ่าน
ถ้าแอปขององค์กรคุณไม่ได้อัปเกรดมาเกินสองปี ขั้นแรกที่ทำได้ทันทีคือถามทีมที่ดูแลว่าใช้ React Native เวอร์ชันไหน และ targetSdkVersion อยู่ที่เท่าไหร่ ถ้า target ห่างจาก Android รุ่นล่าสุด (ตอนนี้คือ 36) เกินสองรุ่น ถึงเวลาวางแผนแล้ว และถ้าแอปอยู่บน Play Store แต่ต่ำกว่า 35 ผู้ใช้ใหม่บน Android รุ่นใหม่เริ่มหาแอปไม่เจอแล้ว
คำถามที่พบบ่อย
- อัปเกรด React Native ใช้เวลานานแค่ไหน
- ขึ้นกับว่าห่างจากเวอร์ชันปัจจุบันแค่ไหนและมี library มากน้อยเท่าไหร่ แอปที่ตามหลังไม่กี่เวอร์ชันมักจบในไม่กี่สัปดาห์ ส่วนแอปที่ค้างมาหลายปีและมีหน้าจอหลายสิบหน้าอย่างในบทความนี้ ใช้เวลาราวสองถึงสามเดือนรวมการทดสอบ
- ถ้าไม่อัปเกรดจะเกิดอะไรขึ้น
- แอปที่ติดตั้งอยู่แล้วยังเปิดใช้ได้ ถ้าอยู่บน Play Store จะส่งอัปเดตไม่ได้จนกว่าจะ target API ตามเกณฑ์ของปีนั้น และถ้าต่ำกว่าเกณฑ์ขั้นต่ำ แอปจะไม่แสดงให้ผู้ใช้ใหม่ที่ใช้ Android รุ่นใหม่กว่าเห็น ส่วนแอปใช้ภายในจะค่อย ๆ มีปัญหากับมือถือรุ่นใหม่ และแก้ยากขึ้นทุกปี
- แอปที่ใช้ภายในองค์กร ไม่ได้ขึ้น Store ต้องอัปเกรดไหม
- ต้อง แม้ไม่มีเส้นตายของ Google Play มาบังคับ แต่มือถือที่ใช้งานจะทยอยเป็นรุ่นใหม่ Android เปลี่ยนเรื่องสิทธิ์และเกณฑ์ขั้นต่ำทุกปี และเครื่องมือ build เก่าจะตั้งบนเครื่องนักพัฒนาได้ยากขึ้นเรื่อย ๆ แอปในบทความนี้ก็เป็นแอปใช้ภายในองค์กร
- ควรทิ้ง React Native แล้วเขียนใหม่ด้วย Flutter เลยไหม
- ไม่จำเป็น การอัปเกรดเก็บ business logic และหน้าจอทั้งหมดที่ผ่านการใช้งานจริงไว้ได้ ส่วนการเขียนใหม่ต้องทำทุกหน้าจอและทดสอบทุก flow ใหม่หมด ซึ่งแพงกว่าและเสี่ยงกว่า ยกเว้นแอปที่ตั้งใจจะรื้อ UX ครั้งใหญ่อยู่แล้ว ส่วนแอปใหม่ที่ยังไม่ได้เลือก ดูเกณฑ์เลือกระหว่าง Flutter กับ React Native
- แอป iOS ต้องอัปเกรดแบบเดียวกันไหม
- ต้อง App Store ก็กำหนดให้ build ด้วย Xcode และ SDK รุ่นใหม่ตามรอบเช่นกัน งานในบทความนี้ทำเฉพาะ Android ตามขอบเขตของลูกค้า แต่ถ้าแอปมีทั้งสองระบบ การอัปเกรดพร้อมกันในรอบเดียวคุ้มกว่า เพราะโค้ด JavaScript ส่วนใหญ่ใช้ร่วมกัน
อยากให้ช่วยดูเคสของคุณโดยเฉพาะไหม
เล่าโจทย์มาคร่าว ๆ ได้เลย เราตีกรอบงบและระยะเวลาให้ฟรีตั้งแต่คุยครั้งแรก