惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
腾讯CDC
V
V2EX
Martin Fowler
Martin Fowler
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Check Point Blog
博客园 - 【当耐特】
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
K
Kaspersky official blog
Security Latest
Security Latest
H
Help Net Security
博客园_首页
美团技术团队
Spread Privacy
Spread Privacy
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
S
SegmentFault 最新的问题
G
Google Developers Blog
NISL@THU
NISL@THU
爱范儿
爱范儿
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security @ Cisco Blogs
Schneier on Security
Schneier on Security
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
V
Vulnerabilities – Threatpost
W
WeLiveSecurity
P
Palo Alto Networks Blog
G
GRAHAM CLULEY
Hacker News: Ask HN
Hacker News: Ask HN
I
InfoQ
The Cloudflare Blog
F
Full Disclosure
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
React Native iOS Push Notifications: почему push не сохраняются в background/killed state
Юрий · 2026-06-01 · via Все публикации подряд на Хабре

Средний

10 мин

5.6K

Введение

Материал написан на основе реального кейса интеграции Mindbox SDK в React Native приложение, но все описанные проблемы и решения универсальны - они касаются любого push-провайдера и любого проекта, где уведомления должны работать в killed state на iOS.

Полагаю, вы законно спросите: А какая задача вообще стояла? Отвечаю, необходимо было не просто показать пользователю push-сообщение, а нужно было эти пуши сохранить в памяти телефона, да ещё и в состоянии, когда приложение закрыто.

Я не буду писать о том, как это сделать, так как это выходит за рамки статьи, а расскажу о проблемах, когда казалось бы, что все обработчики написаны, React-компоненты созданы, push-сообщения отправлены и даже отображаются на экране мобильного телефона, но вот незадача - они никак не обрабатываются и где искать проблему совершенно непонятно, так как логи пусты. Итак, приступим.

Сценарий, через который рано или поздно проходит почти каждый React Native разработчик, выглядит примерно так: push-уведомления отлично работают на Android; на iOS всё нормально, пока приложение открыто.

А теперь сценарий, когда приложение работает в background: push приходит, но данные не сохраняются. Аналитика не отправляется. React Native не получает событие. Кастомный payload теряется. Local storage пустой. Бизнес-логика не выполняется.

Первая реакция обычно такая: «Наверное, опять React Native что-то сломал».

Но проблема почти никогда не находится на уровне JavaScript. Настоящая причина лежит гораздо глубже: iOS process lifecycle, Notification Service Extensions, App Groups, shared storage, sandbox architecture, Xcode targets, APNS delivery pipeline. Именно в этот момент многие React Native разработчики впервые сталкиваются с тем, что mobile engineering — это не только JS.

В этой статье разберём: почему Android и iOS ведут себя по-разному; как на самом деле работает push pipeline на iOS; почему AppDelegate не всегда вызывается; зачем нужен Notification Service Extension; почему App Groups критически важны; как правильно сохранять push в killed state; какие ошибки ломают всё чаще всего.

Почему на Android всё работает «само»

Скажу сразу, что я немного лукавлю - в Android всё не работает само, но я осознанно пошёл на это допущение, так как сегодня мы говорим про iOS. Возможно, я напишу ещё одну статью о проблемах интеграции push-сервисов в Android-приложение, ну а пока так.

Android гораздо более либерален в background execution. Даже если приложение убито, Android всё ещё может запускать background services, будить process, выполнять Headless JS, обрабатывать FCM payload, писать данные в storage. React Native разработчик привыкает к тому, что push пришёл - JS получил данные — всё сохранилось. На iOS архитектура совершенно другая.

Как работает push pipeline на iOS

На iOS способ обработки push зависит от состояния приложения. И это фундаментально важно.

Foreground

Когда приложение открыто, цепочка выглядит предсказуемо: APNS → AppDelegate → React Native bridge → JavaScript. В этом режиме всё просто. Ваш AppDelegate получает push:

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
    let userInfo = notification.request.content.userInfo
    
    savePush(userInfo)
    notifyReactNative()
}

Приложение живо. React Native bridge жив. JS runtime жив. Можно спокойно сохранять данные, обновлять state, отправлять analytics, вызывать NativeModules, работать с local storage.

Background

Когда приложение свёрнуто, ситуация уже сложнее. Иногда iOS всё ещё позволяет AppDelegate получить push. Иногда нет. Это зависит от типа push, system state, memory pressure, background execution policy, payload. Именно поэтому background-поведение на iOS часто кажется нестабильным - оно действительно нестабильно по дизайну. Система сама решает, дать приложению процессорное время или нет, и её решение непредсказуемо для разработчика.

Killed state

А вот здесь начинается самое интересное. Когда пользователь свайпает приложение из списка recent apps - app process уничтожается. AppDelegate больше не существует. React Native bridge не существует. Hermes/JSC runtime не существует. JavaScript не выполняется. И в этот момент многие разработчики продолжают ожидать: «push придёт — JS обработает данные». Но обрабатывать данные уже некому.

Почему push всё ещё отображается

Потому что push показывает сама iOS. Операционная система читает:

{
  "aps": {
    "alert": {
      "title": "Новое сообщение",
      "body": "Привет"
    }
  }
}

И отображает notification UI. Но ваш код при этом может вообще не запускаться. Это ключевой момент, который трудно принять после Android-опыта: уведомление видно, значит кто-то же его показал, значит приложение работало? Нет. Показала система. Ваш процесс в этом не участвовал.

Решение - Notification Service Extension

Apple предусмотрела специальный механизм: Notification Service Extension. Это отдельный mini-process. Не часть приложения. Не часть React Native. Не часть AppDelegate. Это отдельный process внутри iOS, который система запускает по своим правилам.

Extension умеет модифицировать push, подгружать изображения, изменять текст notification, расшифровывать payload, сохранять данные, подготавливать rich notifications. И самое главное: extension может запускаться даже когда основное приложение убито. Это именно то, что нам нужно.

Что многие понимают неправильно

Очень многие думают: extension - это часть app. Но это совсем не так. У extension свой lifecycle, свой process, свой sandbox, свой bundle id, свои entitlements. По сути это отдельное mini-приложение со своим жизненным циклом. Система запускает его независимо, даёт около 30 секунд на работу и так же независимо убивает. Основное приложение может никогда не узнать, что extension вообще запускался.

Почему storage внезапно ломается

Вот здесь находится главный баг, который ломает голову новичкам. Допустим, extension получил push и сохранил данные:

UserDefaults.standard.set(...)

А потом основное приложение пытается прочитать через UserDefaults.standard - и ничего не находит. Причина проста и от этого ещё более обидная: UserDefaults.standard у extension и UserDefaults.standard у app - это разные sandbox containers. Физически разные файлы на диске, в разных директориях, с разными правами доступа. Extension честно записал данные, приложение честно пошло их читать, но каждый смотрит в свой собственный изолированный контейнер.

App Groups

Для решения этой проблемы Apple придумала App Groups — shared container между приложением, extensions, widgets и другими targets. Это общая область файловой системы, доступная всем участникам группы. Например: group.cloud.Mindbox.com.example.exampleapp. Тогда UserDefaults(suiteName: "group.cloud.Mindbox.com.example.exampleapp") будет общим storage и для main app, и для Notification Service Extension.

В нашем случае с Mindbox это особенно важно: SDK должен иметь возможность записать данные о полученном пуше в момент, когда основное приложение ещё не запущено, а затем передать их приложению, когда пользователь откроет уведомление.

Самая частая ошибка

Разные App Groups. App использует UserDefaults(suiteName: "group.cloud.Mindbox.com.example.exampleapp"), а extension использует UserDefaults(suiteName: "group.cloud.Mindbox.com.mindbox.exampleRN"). В результате extension пишет в один container, app читает из другого. Визуально это выглядит как «push не сохраняется в background». Хотя push сохраняется. Просто не туда.

Эта ошибка особенно коварна тем, что на первый взгляд всё настроено правильно. Идентификаторы групп похожи, оба зарегистрированы в developer portal, entitlements на месте. Но одна опечатка в строке - и данные уходят в никуда. А дебажить это сложно, потому что напрямую заглянуть в shared container extension во время его работы почти невозможно.

Почему это особенно больно в React Native

Потому что React Native скрывает native layer. Пока всё работает - это удобно. Но как только начинается background execution, extensions, APNS lifecycle, shared storage, native debugging - JS abstraction перестаёт помогать. Приходится понимать, как реально устроена mobile OS. И это момент, когда многие RN-разработчики впервые открывают Xcode не чтобы поменять иконку, а чтобы пройтись дебаггером по Swift-коду extension в режиме реального времени.

mutable-content — обязательный параметр

Notification Service Extension НЕ запускается автоматически. Для этого push должен содержать:

{
  "aps": {
    "mutable-content": 1
  }
}

Без этого ключа extension никогда не будет вызван. Система просто не знает, что вы хотите модифицировать уведомление перед показом. Это одна из самых частых причин, по которой разработчик создал extension, написал код, настроил App Groups, а ничего не работает. Проверьте payload. Скорее всего, mutable-content отсутствует или равен нулю. При интеграции с Mindbox убедитесь, что кампании на стороне провайдера отправляют этот флаг — без него extension не запустится ни при каких обстоятельствах.

Почему недостаточно просто создать папку extension

Очень распространённая ошибка. Разработчик создаёт ios/MindboxNotificationServiceExtension, кладёт туда Swift-файлы - и думает что extension готов. Но iOS использует не папки. iOS использует Xcode targets. Это совершенно другой уровень абстракции.

В Xcode должен существовать полноценный NotificationServiceExtension target с bundle id, entitlements, signing, build phases, embed configuration, pods. Без этого ваши Swift-файлы - просто текст на диске. Они не компилируются, не линкуются, не попадают в финальную сборку. Система просто не знает об их существовании.

Почему Podfile начинает ломаться

Extension - это отдельный target. И ему нужны собственные pod dependencies. Например:

target 'MindboxNotificationServiceExtension' do
  pod 'MindboxNotifications'
end

Без этого extension может не собраться, не линковаться, silently fail. Самое неприятное - silently fail. Ошибки линковки extension не всегда видны при сборке основного таргета. Вы запускаете приложение, оно работает, а extension молча падает при первом же пуше, и вы об этом узнаете только от пользователей.

Ещё одна ловушка - duplicate native files

Так уж получилось, что работая с пушами, я параллельно производил миграцию на RN с версии 0.74 на 0.78 и столкнулся с интересной проблемой, которая оказалась, что встречается довольно часто.

Например, у вас есть ios/NotificationModule.swift и одновременно ios/ExampleApp/NotificationModule.swift. Если внутри обоих файлов объявлен класс с одинаковой Objective-C аннотацией @objc(NotificationModule), то iOS может компилировать обе версии, использовать случайную, получать duplicate symbols, работать нестабильно.

Эта нестабильность особенно коварна. Локально на дебаг-сборке может подхватываться один файл и всё работать. На CI собирается release-сборка с другими флагами оптимизации — и линковщик выбирает другой файл. Поведение меняется от сборки к сборке. Clean build, pod install, release archive - каждый раз лотерея. Единственный способ избежать этого - аудит исходников после каждой миграции и безжалостное удаление дубликатов.

Когда Automatic Signing ломает всё

Я долго думал, включать ли этот раздел в статью - лично я с таким на практике не сталкивался, а узнал об этой проблеме из доклада от Mad Brains Техно «Как кастомизировать пуши в iOS». Вопрос показался мне интересным, и я решил в нём разобраться.

Кстати, рекомендую данный доклад для общего развития, ссылку оставлять не буду, но вы его легко можете найти сами.

На первый взгляд кажется, что Automatic Signing решает все проблемы. Xcode сам управляет сертификатами, provisioning profiles, entitlements. Но как только появляются extensions, автоматика может давать сбой.

Первый сценарий: Xcode «забывает» entitlements. Вы создали extension, добавили App Groups capability, проверили - всё на месте. Но в project.pbxproj для extension target может отсутствовать параметр CODE_SIGN_ENTITLEMENTS. Файл .entitlements физически существует на диске, но не используется при подписи. Результат: aps-environment не попадает в финальную сборку, и push не приходит, даже когда всё остальное настроено правильно.

Второй сценарий: сложная структура сертификатов. В некоторых проектах проще один раз зайти в Apple Developer Portal, вручную создать Provisioning Profile для extension, явно указав в нём App Groups и Push Notifications capability, скачать его и прописать в Xcode вручную на вкладке Signing (Manual). Это даёт полный контроль и устраняет элемент неожиданности.

Правило: если настроили всё по инструкции, а extension молчит или сыплются ошибки подписи - первым делом отключите Automatic Signing для extension target. Проверьте CODE_SIGN_ENTITLEMENTS в project.pbxproj. Убедитесь, что provisioning profile содержит нужные App Groups. Иногда пять минут в developer.apple.com экономят часы дебага в Xcode.

Embed App Extensions - плагин должен быть внутри

Ещё одна ошибка, которую невозможно заметить визуально. Недостаточно, чтобы extension target существовал в проекте и успешно компилировался. Расширение должно быть внедрено в основное приложение.

Технически это означает, что бинарный файл расширения (.appex) должен быть скопирован внутрь .app бандла основного приложения. Без этого iOS просто не найдёт расширение при получении пуша. Push с mutable-content: 1 придёт, но запускать будет нечего.

В Xcode это настраивается через Build Phases основного таргета. Нужно открыть таргет приложения, перейти на вкладку Build Phases, найти секцию «Embed Foundation Extensions» и убедиться, что ваш MindboxNotificationServiceExtension присутствует в списке. Если его там нет - добавить через плюсик.

Это особенно коварно тем, что никаких ошибок сборки не возникает. Расширение компилируется, подписывается, всё зелёное. Но оно лежит отдельно от приложения, и система о нём не знает. Вы отправляете тестовый push, ждёте срабатывания, а в консоли - тишина. Потому что запускать нечего.

Проверить, что расширение попало в бандл, можно простым способом: собрать .ipa или .app, открыть его содержимое и найти папку PlugIns. Внутри должен лежать MindboxNotificationServiceExtension.appex. Если папка пустая или отсутствует - вы не заembedили расширение.

Что в итоге должно получиться

Правильный pipeline выглядит так: APNS → Notification Service Extension → Shared App Group UserDefaults → Main App → React Native bridge → JavaScript. Каждый этап должен быть настроен явно, никакой магии не происходит. Если хотя бы одно звено выпадает - данные теряются.

Чек-лист рабочего решения

Чтобы push-уведомления корректно сохранялись на iOS в background/killed state, нужно последовательно пройти десять шагов.

Шаг первый: добавить Notification Service Extension target в Xcode. Не папку, не Swift-файлы рядом с проектом — именно target.

Шаг второй: добавить "mutable-content": 1 в payload каждого пуша, который должен обрабатываться extension.

Шаг третий: создать одну общую App Group, например group.cloud.Mindbox.com.example.exampleapp, и зарегистрировать её в developer portal.

Шаг четвёртый: использовать её везде — в AppDelegate, в NotificationModule, в Extension, в entitlements обоих таргетов. Никаких вариаций, никаких похожих строк. Ровно один и тот же идентификатор.

Шаг пятый: использовать shared UserDefaults исключительно через suiteName:

UserDefaults(suiteName: "group.cloud.Mindbox.com.example.exampleapp")

Никаких UserDefaults.standard в контексте передачи данных между процессами.

Шаг шестой: правильно подключить extension target в настройках основного таргета - Embed App Extensions должен содержать ваш extension.

Шаг седьмой: добавить extension target в Podfile с корректными зависимостями и директивой inherit! :search_paths.

Шаг восьмой: проверить, что extension присутствует в списке Embedded App Extensions после сборки. Это можно посмотреть в составе .app-бандла.

Шаг девятый: удалить duplicate native files. После каждой миграции проходиться по дереву исходников и безжалостно вычищать дубликаты модулей.

Шаг десятый: проверить, что extension реально запускается. Добавьте print("🔥 EXTENSION STARTED") в метод didReceive и подключитесь через Console.app к процессу extension при отправке тестового пуша. Пока не увидели лог - не считайте, что extension работает.

И последний совет: иногда лучше тестировать на живом устройстве. Если вы уверены, что все сделали правильно, не поленитесь - соберите проект и протестируйте приложение через TestFlight.

Итог

Главная мысль, которую важно понять: проблема background push на iOS - это не React Native проблема. Это iOS architecture, process lifecycle, sandbox isolation, native mobile engineering. И именно в этот момент React Native developer начинает превращаться из JS-разработчика в mobile engineer. Потому что настоящие production-проблемы почти всегда живут глубже JavaScript слоя.

По традиции, я бы попросил вас подписаться на мой канал, чтобы поддержать автора, но теперь даже и не знаю. В общем, как говорит один из моих любимый ютуб-блогеров:

stay in touch!