728x90

객체지향 프로그래밍을

"잘" 해보기위해

처음부터 다시 공부중이다.

 

상속을 쓰다보면

늘 "객체지향"라는 개념이 혼동되었다.

 

내 기억속의 "객체지향"은

마치 자동차를 만드는 작업과 같았다.

 

예를들면,

스팅어가 탄생하는 과정을 한번 그려보자.

 

[상속의 개념을 적용한 예시]

a: "우리 스팅어라는 자동차를 만들거야,

아주 빠르고 스포티한 자동차지"

b: "그러면 엔진인 gv80에서 상속받고

타이어랑 엔진은 g70에서 상속받고...

뼈대는 탄탄하게 모하비에서 상속받도록 할까요?"

 

하지만, 실제로는 그렇지않다.

아주 작은 부품들이 모여서 하나의 덩어리 부품를 만들고.

그 덩어리 부품들이 모여서 하나의 자동차를 만드는 것이다.

 

이렇게 부품을 만들고

부품을 가지고 새로운 부품을 "구성" 하는것은

Composition(구성) 이라고 한다.

 

상속이 쓸모 없느것은 아니지만

먼저 어떻게 Composition을 할지 생각해보고

다음에 그러면 공통적인 무언가만 상속하는 형태로

가져가는게 좋지 않을까

하는 개발론적인 생각을 해보았다.

 

그럼, Composition(구성)의 개념을 적용하여

스팅어와 소나타를 만드는 과정을 코드로 살펴보자.

Github링크: https://github.com/jaekwangLee/oop_js_study

{
  interface Engine {
    run(): boolean;
    stop(): boolean;
    pressure: string; // 'weak' | 'strong' | 'normal';
  }

  interface Mission {
    mode: string; // auto | manual
    change(gear: number): number;
    clutch(): void;
    getGear(): number;
  }

  interface Frame {
    material: string; // Materials
    strength(): string;
    size(): { length: number; width: number; height: number };
    crash(): string;
  }

  interface Tire {
    brand: string;
    purpose: string;
    count: number;
    role(): void;
    pause(): void;
  }

  type Materials = "stainless" | "steel" | "aluminium";
  type TirePurpose = "winter" | "summer" | "for season";

  class GDI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "strong";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine is working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class MPI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "weak";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class CVT implements Mission {
    mode = "";
    constructor(private gear: number, mode: "manual" | "auto") {
      this.mode = mode;
    }

    clutch = () => {
      console.log("press clutch!! ䷮");
    };

    change = (nextGear: number) => {
      if (nextGear > this.gear) return this.gear;
      if (nextGear <= 0) return this.gear;

      this.clutch();

      this.gear = nextGear;
      return this.gear;
    };

    getGear = () => {
      return this.gear;
    };
  }

  class BodyOnFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "steel";

    strength = () => {
      return "hard";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  class UniBodyFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "aluminum";

    strength = () => {
      return "normal";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  // 작업중
  class FourSeasonTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;
    characters = [];

    constructor(brand: string, purpose: string, count: number) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class SummerTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;

    constructor(
      brand: string,
      purpose: string,
      count: number,
      private characters?: Array<string>
    ) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class Car {
    constructor(
      private tire: Tire,
      private frame: Frame,
      private engine: Engine,
      private mission: Mission,
      private name: string
    ) {}

    turnOn = () => {
    console.log('Hi, myname is ', this.name);
      this.engine.run();
    };

    turnOff = () => {
      this.engine.stop();
    };

    start = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() <= 0) {
        console.log("please, change the gear");
      } else {
          console.log("car is starting ~~ 🚗");
        this.tire.role();
      }
    };

    stop = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() > 0) {
        this.mission.clutch();
        this.mission.change(0);
      }

      console.log("car is stopping ~~ 🚗");
      this.tire.pause();
    };
  }

  // make car !
  // tires
  const normalTires = new FourSeasonTire("Nexen", "four season", 4);
  const summerTires = new SummerTire("Kumho", "summer", 4);
  // engine
  const gdi = new GDI(320, 52);
  const mpi = new MPI(180, 23);
  // mission
  const cvt4 = new CVT(6, "auto");
  const cvt16 = new CVT(12, "auto");
  // frame
  const unibody = new UniBodyFrame(4950, 1900, 1720);
  const bodyon = new BodyOnFrame(4920, 1870, 1700);

  const sonata = new Car(normalTires, bodyon, mpi, cvt4, "sonata");
  const stinger = new Car(summerTires, unibody, gdi, cvt16, "stinger");

  sonata.turnOn();
  sonata.start();
  console.log('---------------')
  stinger.turnOn();
  stinger.start();
}

조금 길어보이지만 Composition의 개념을 통해

엔진/미션/타이어/뼈대로 구성된 자동차를 만드는 과정이다.

 

코드에서 알수 있듯이

각 부품멸 "설계도"에 해당하는 interface를 만들고

"설계도"를 가지고 부품(class)를 만들었다.

 

위에서는 두가지정도씩 부품을 만들었지만,

설계도를 가지고 얼마든지 다양한 부품을 찍어낼수있다.

 

그리고 각 설계도면을 가지고 만든 부품을

Car 라는 최종 부품, 산출물이 받게된다.

 

Car는

각 부품이 interface로 정의되어

아주 유연하게 설계되었다.

마치 그 유명한 "붕어빵 기계"와 같다.

 

위 코드를 실행한 결과

Hi, myname is  sonata
heating ...♨
engine working with  180 23
use power  weak
rolling ...🚗
car is starting ~~ 🚗
---------------
Hi, myname is  stinger
heating ...♨
engine is working with  320 52
use power  strong
rolling ...🚗
car is starting ~~ 🚗

이렇게 귀여운

자동차 두대의 동작을 살펴볼 수 있다.

728x90
반응형
728x90
Data Class Xcode Key Raw Info.plist Key
Apple Music Privacy - Media Library Usage Description NSAppleMusicUsageDescription
Bluetooth Privacy - Bluetooth Peripheral Usage Description NSBluetoothPeripheralUsageDescription
Calendar Privacy - Calendars Usage Description NSCalenedarUsageDescription
Camera Privacy - Camera Usage Description NSCameraUsageDescription
Contacts Privacy - Contact Usage Description NSContactsUsageDescription
Health Privacy - Health Share Usage
Description Privacy - Health Update Usage Description
NSHealthShareUsageDescription
NSHealthUpdateUsageDescription
Home Privacy - HomeKit Usage Description NSHomeKitUsageDescription
Location Privacy - Location Always Usage
Description Privacy - Location When In Use Usage Description
NSLocationAlwaysUsageDescription
NSLocationWhenInUseUsageDescription 
Microphone Privacy - Microphone Usage Description NSMicrophoneUsageDescription
Motion Privacy - Motion Usage Description NSMotionUsageDescription
Photos Privacy - Photo Library Usage Description NSPhotoLibraryUsageDescription
Reminders Privacy - Reminders Usage Description NSRemindersUsageDescription
Siri Privacy - Siri Usage Description NSSiriUsageDescription
Speech
Recognition
Privacy - Speech Recognition Usage Description NSSpeechRecognitionUsageDescription
TV Provider
Account
Privacy - TV Provider Usage Description NSVideoSubscriberAccountUsageDescription

 

NSAppleMusicUsageDescription : 미디어 라이브러리 접근

NSBluetoothPeripheralUsageDescription : 블루투스 인터페이스 접근

NSCalenedarUsageDescription : 달력 접근

NSCameraUsageDescription : 카메라 접근

NSContactsUsageDescription : 연락처에 접근

NSHealthShareUsageDescription : 헬스 데이터에 접근

NSHealthUpdateUsageDescription : 건강 데이터에 접근

NSHomeKitUsageDescription : 홈킷 설정 데이터에 접근

NSLocationAlwaysUsageDescription : 위치정보 접근 (항시 허용)
NSLocationWhenInUseUsageDescription : 위치정보 접근 (사용시 허용)

NSMicrophoneUsageDescription : 마이크 접근

NSPhotoLibraryUsageDescription : 사진 라이브러리 접근

NSSiriUsageDescription : 시리 접근

 

 

예전과 다르게

앱 배포할때 필수적으로 세팅해야되는 사항들이

많아졌다.

 

개인정보의 가치가

점점 소중해지고

쾌적한 앱 환경 조성을 위해

발전하고 있는 과정이라고 생각한다.

 

(개발자에겐 불편)

728x90
반응형
728x90

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

 

푸시 알림이

ㄱ) foreground

ㄴ) background

ㄷ) quit state

에서 모두 정상적으로 수신되는것을

분명히 확인했었다

 

라고 착각을 하고있었는데

앱 배포 직전

검토 과정에서

iOS에 알림이 수신은 되지만

진동이 울리지 않음을 발견하였다.

 

위 문제를 해결하기 위해

아래와 같은 방법으로 접근하였다.

 

1. 내가 설정을 덜 한것은 없는지

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

3. vibration 기능의 접근 권한이 문제인지

4. setBackgounrMessageHandler 수신 부분이 문제인지

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

 

1. 내가 설정을 덜 한것은 없는지

rnfirebase docs를 통해 체크한 결과

headless 상태에 대한 처리가 미흡해 보였다.

// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';

messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Message handled in the background!', remoteMessage);
});

messaging()
  .getIsHeadless()
  .then(isHeadless => {
    // do sth with isHeadless
  });
  
function HeadlessCheck({ isHeadless }) {
  if (isHeadless) {
    // App has been launched in the background by iOS, ignore
    return null;
  }

  return <App />;
}

function App() {
  // Your application
}

AppRegistry.registerComponent('app', () => HeadlessCheck);
// appDelegate.m

// add this import statement at the top of your `AppDelegate.m` file
#import "RNFBMessagingModule.h"

// in "(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions" method
// Use `addCustomPropsToUserProps` to pass in props for initialization of your app
// Or pass in `nil` if you have none as per below example
// For `withLaunchOptions` please pass in `launchOptions` object
NSDictionary *appProperties = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions];

// Find the `RCTRootView` instance and update the `initialProperties` with your `appProperties` instance
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                             moduleName:@"nameOfYourApp"
                                             initialProperties:appProperties];

위 세가지 설정들에 대해서 다시 한번

검토를 하였다.

 

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

제법 많은 비슷한 내용들이 있었다.

그 중에서 내가 찾은 솔루션은

https://github.com/invertase/react-native-firebase/issues/5656

위의 방법인데

이것은  3,4,5의 과정을 걸쳐서 찾은 솔루션이다.

 

3. vibration 기능의 접근 권한이 문제인지

react-native에 vibration이라는 기능이 내포되어있다.

이 마저도 해보려고하니 잘못된 정보들이 너무나도 많았다.

임시 버튼을 만들어

기능이 잘 동작하는지만 확인하였다.

import { Platform, Vibration } from 'react-native';

// test code
const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

4. setBackgounrdMessageHandler 수신 부분이 문제인지

화면을 열어보면 푸시가 와있어서

setBackgounrdMessageHandler 리스너에

문제가 있으리라고는 생각해보지 않았었다.

 

하지만, 디바이스에서는 수신했더라도

앱 내에 setBackgounrdMessageHandler 리스너가

실제로 잘 동작하고 있는지는 검토해볼 필요가 있었다.

messaging().setBackgroundMessageHandler(async remoteMessage => {
  umm(); // vibration
  console.log('Message handled in the background!', remoteMessage);
});

const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

위와 같이 임시 코드를 작성해서

진동이 오는지 체크해본 결과

위의 리스너가 제대로 동작하고 있지 않음을 발견했다.

 

리스너가 제대로 동작하지 않는다면

설정의 문제 혹은 발송의 문제

둘중에 하나라고 결론을 냈다.

 

그런데 아까 분명히

설정의 문제는 확인을 완료했다.

그러면 발송의 문제일 것이다.

 

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

2번에서 언급했던

https://github.com/invertase/react-native-firebase/issues/5656

위의 링크에서 알맞는 발송 포맷을 발견했다.

 

추후, 가이드를 다시보니 업데이트가 된것인지

위 github 링크와 동일한 내용으로 발송 포맷이 나와있었다.

 

github 마지막 코멘트 업데이트 일자가

patels9-vf commented 7 hours ago

겨우 7시간전이 아닌가...

위 이슈가 최근까지도 이어지고있었나보다.

 

공식 가이드에서는

아래와 같이 푸시메시지를 발송하라고 한다.

admin.messaging().send({
  data: {
    //some data
  },
  apns: {
    payload: {
      aps: {
        contentAvailable: true,
      },
    },
    headers: {
      'apns-push-type': 'background',
      'apns-priority': '5',
      'apns-topic': '', // your app bundle identifier
    },
  },
  //must include token, topic, or condition
  //token: //device token
  //topic: //notification topic
  //condition: //notification condition
});

 

다만, 위의 메시지는

data-only에 해당하는 것이기도 하고

보다 많은 케이스에 대비하고자

github의 가이드를 따랐다.

 

const message: any = {
  token,
  notification: {
    title,
    body,
  },
  data: {
    title,
    body,
  },
  android: {
    priority: 'high',
    notification: {
      title,
      body,
      sound: 'default'
    }
  },
  apns: {
    headers: {
      'apns-priority': '5',
    },
    payload: {
      aps: {
        sound: 'default',
        // contentAvailable: 1,
        'content-available': 1,
      }
    }
  }
};
admin.messaging().send(message);

 

위 방법으로 문제없이 메시지가 수신됨을 확인하였다.

 

 

** 번외 **

나는 애플워치를 사용하는데

핸드폰에서 알림이 안울리고

계속 애플워치에서 알림이 울려서

블루투스를 끊고 핸드폰에서 알림이 오는지에 대한

확인이 필요하였다.

 

테스트 결과 수신은 됐다.

다만, 어느정도의 딜레이가 발생했다.

 

가이드에 따르면 priority 설정된

푸시 메시지는

8seconds 정도의 간격으로만

발송이 가능한듯하다.

 

 

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

728x90
반응형
728x90

[ react-native-code-push 적용하기 ]

https://honeystorage.tistory.com/290

 

[ MS, appcenter 공식 Document ]

https://docs.microsoft.com/ko-kr/appcenter/distribution/codepush/

 

지난 포스팅에 이어

실무에서 사용하기 위한

몇가지 업데이트 사항을 알아보고자 한다

 

1. Mandatory (필수 적용 옵션)

배포 내용에 따라 주요 업데이트때와 마찬가지로

필수적으로 업데이트를 했으면 하는

경우가 발생할 수 있다.

 

배포간에 Mandatory옵션을 사용하면

"업데이트 / 나중에"와 같이 선택지를 주는게 아니라

"업데이트" 선택지 하나만 줄 수 있다.

 

// 일반적인 배포
appcenter codepush release-react -a username/appname -d Production

// 필수사항 배포
appcenter codepush release-react true -a username/appname -m -d Production

"-m" 부분이 추가되었다.

 

코드상에서 updateDialog를 수정해서

사용자에 보여줄 텍스트를 커스텀 할 수도있다.

codePush.sync({
 updateDialog: {
   titie: '업데이트 하세요!',
   mandatoryUpdateMessage: '필수 업데이트입니다. 진행해주세요.',
   mandatoryContinueButtonLabel: '업데이트',
 },
 mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
});

 

 

*** 기타 내용 업데이트 예정

728x90
반응형
728x90

패키지를 추가하거나

native코드를 수정하는 등의

큰 변화를 주지않고

 

js, img, css 를 수정하는 배포에대해

빌드를 하지 않고도 배포할수 있게 해주는

code-push기능 적용 일기이다.

 

참고 링크 1 Weiji 웨이지 님의 블로그

참고 링크 2 Gale Lee 님의 블로그

참고링크 3 appcenter 공식 문서

본 작업은 ,  react-native 0.66 버전에서 진행되었습니다.

 

1. 설치

npm i -g appcenter-cli

 

2. 로그인

appcenter login

위 명령어를 입력하면

? Enable Temporary(Y/n)? 라는 질문이 나타난다.

원투데이 개발할게 아니므로 당연히 n

 

그러고나면 terminal에

?Access code from brwoser이라는 질문과함께

연동된 페이지가 나타나며

구글/깃헙/페북 등 로그인 수단을 선택할것이라고 뜬다.

 

나는 github을 선택했고, username을 설정하고나니

code가 떴다.

코드 복붙

그러면 로그인 완료

 

3. 로그인 확인

appcenter profile list

위 명령어로 로그인된 계정의

Username, Display Name, Email 등을 확인할 수 있다.

 

4. 서비스 등록하기 (대소문자 주의)

appcenter apps create -d appname-aos -o Android -p React-Native
appcenter apps create -d appname-ios -o iOS -p React-Native

이름은 자유지만 android/ios 구분해서 짓기

 

5. 등록된 서비스 조회

appcenter apps list

 

6. 홈페이지 접속해보기

https://appcenter.ms/apps

 

7. react-native-code-push 설치하기

npm i --save react-native-code-push

 

8.  배포단계 설정하기

appcenter codepush deployment add -a username/appname-aos Staging
appcenter codepush deployment add -a username/appname-aos Production
appcenter codepush deployment add -a username/appname-ios Staging
appcenter codepush deployment add -a username/appname-ios Production

이렇게 배포단계에 대한 네이밍을 세팅해두면

appcenter codepush deployment list -a username/appname-aos

이런 방식으로 현재 진행중인 상태를 조회할수 있다고 한다.

 

9. 앱의 배포를 위해 key 조회하기

appcenter codepush deployment list -a username/appname-aos -k
appcenter codepush deployment list -a username/appname-ios -k

 

10. ios 설정

- 라이브러리를 설치했으니 pod도 설치해준다.

cd ios && pod install && cd ..

 

- xcode의 AppDelegate.m 수정

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #else
    return [CodePush bundleURL];
  #endif <-- 새로 변경된 파트
  
//#if DEBUG
//  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
//#else
//  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
//#endif <-- 원래 파트
}

 

- app에 AppCenter-Config.plist 추가

NewFile -> Property List -> Save as (AppCenter-Config.plist)

 

- Project > info > Configuration > +

위 이미지처럼 Staging항목을 추가해줍니다.

 

- Info.plist에 Key 추가하기

key: CodePushDeploymentKey / value: ${CODEPUSH_KEY}

를 추가해줍니다.

 

Project > Build Settings > + Add user defined setting을 클릭

 

MULTI_DEPLOYMENT_CONFIG 추가

release $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

staging $(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)

 

같은 방법으로

CODEPUSH_KEY 추가

appcenter codepush deployment list -a username/appname-ios -k

위 명령어로 조회된 키를 

release, staging에 알맞게 입력

 

 

11. android 설정

- android/setting.gradle

// add code push
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')

 

- key 추가하기

먼저, 아래 명령어로 키를 조회

appcenter codepush deployment list -a username/appname-aos -k

아래 내용 추가

// android/gradle.properties

CODEPUSH_DEPLOYMENT_KEY_DEBUG=
CODEPUSH_DEPLOYMENT_KEY_STAGING=????
CODEPUSH_DEPLOYMENT_KEY_PRODUCTION=????

 

- /android/app/build.gradle

// buildTypes
buildTypes {
  debug {
    ...
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_DEBUG
  }
  release {
    ...
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_PRODUCTION
  }
  releaseStaging {
    initWith release
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_STAGING
    matchingFallbacks = ['release']
  }
}




// enableHermes 관련 코드가 있다면
if (enableHermes) {
  ...
  releaseStagingImplementation files(hermesPath + "hermes-release.aar")
} else {
  ... 
}



// 맨 아래에 추가
apply from: "../../node_modules/react-native/react.gradle" <-- 에러나면 주석처리
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"

- MainApplication.java

import com.microsoft.codepush.react.CodePush; // <- add

new ReactNativeHost(this) {
  ...
  
  //add
  @Override
  protected String getJSBundleFile() {
    return CodePush.getJSBundleFile();
  }
}

 

+++ 안드로이드에서 코드푸시가 되지않아 에러 추적을해본 결과

[CodePush] Exception com.microsoft.codepush.react.CodePushUnknownException: Error in getting binary resources modified time

위와 같은 에러가 발견됨

// android/app/build.gradle

defaultConfig {
  ...
  resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis())
}

이 방법으로 해결함

 

설정할게 많다...

 

그치만 우리가 얻는 이득에 비하면 이건 아무것도아니다.

(하, 그래도 이거는 솔직히 할거 너무많다)

 

설정은 끝났다.

업데이트를 수신할건지에 대한 코드만 추가해주면된다.

 

12. 코드 추가하기

(여기서부터는 자유롭게 하면 될것같다)

// 필자는 App.tsx 에서 작업하였다.

import CodePush from 'react-native-code-push';
import { AppState } from 'react-native';

useEffect(() => {
  const staging = AppState.addEventListener('change', (state) => {
    if (state === 'active') codePushSync();
  });
  
  return () => {
    staging.remove();
  }
}, [])

const codePushSync = () => {
  CodePush.sync({
    updateDialog: {
      title: '새로운 업데이트가 존재합니다.',
      optionalUpdateMessage: '지금 업데이트 하시겠습니까?',
      optionalIgnoreButtonLabel: '나중에',
      optionalInstallButtonLabel: '업데이트'
    },
    installMode: CodePush.InstallMode.IMMEDIATE
  })
}



export default CodePush({ checkFreQuency: CodePush.CheckFrequency.MANUAL })(App);

 

빌드파일을 지우고

re-빌드해서 배포테스트를 해보자.

728x90
반응형
728x90

개발중

안드로이드에서 Modal을 닫을때

순간적으로 깜빡이는 이슈가

별견되었다.

 

github. Issue에

많은 솔루션들이 있었지만

 

나는 아래의 방법을 적용하니

깔끔하게 해결되었다.

 

return (
  <Modal 
    style={Style}
    isVisible={visible} 
    animationIn={InEffect}
    animationOut={OutEffect}
    backdropOpacity={Opacity}
    backdropTransitionOutTiming={0} <-- 여기
  >
    <ModalContainer>{Viewer}</ModalContainer>
  </Modal>
);

 

참고링크:

https://github.com/react-native-modal/react-native-modal/issues/268

 

 

추가로 이런 이슈도 있었다.

대부분의 modal라이브러리가 그렇듯

react-naive-modal에도 visible 프로퍼티가 존재한다.

 

반드시 modal의 노출 여부는

이 visible로만 핸들링 해야한다.

아래와 같은 코드에서는 비정상적인 움직임을 보여준다.

 

// 올바른 사례
const [show, setShow] = useState(false);

<Modal
  visible={show}
>
</Modal>



// 잘못된 사례
const [show, setShow] = useState(false);
{
  !!show &&
  <Modal
    visible={show}
  >
  </Modal> 
}

 

어떤 이유에선가 아래와 같이 작업할지도 모른다.

그렇게하면 on/off는 컨트롤 할 수 있겠지만

비정상적인 애니메이션을 보게 될것이다.

728x90
반응형
728x90

이전 글

[1. 객체지향 프로그램의 의미를 알아보자]

https://honeystorage.tistory.com/287

 

아래 내용 관련 Github :)

https://github.com/jaekwangLee/oop_js_study

 

우리는 이전 글에서

객체지향의 탄생에는

절차지향 프로그래밍의 한계가 있음을

알게되었다.

 

이제는 구체적으로

객체지향의 특징들을 알아보자.

 

흔히 4가지 특징이 있다고 한다.

1. 캡슐화 : 내부의 특성은 안으로 숨기고 사용자에게는 사용법만을 노출시킨다.

2. 상속 : 부모의 특성을 자식이 물려받음

3. 다형성 : 하나의 클래스나 메소드가 다양한 방식으로

4. 추상화 : 중요한 정보만을 표현하여 공통의 속성이나 이름을 묶어 이름 붙임

 

한글로만 봐도 어려운 단어들이지만

하나씩 이해해보자

실제로 써보면

절차지향 프로그래밍에서는 못느꼈던

새로운 희열을 느낄수있다.

 

다시, 이전의 예시코드를 살펴보자.

이 짧은 예시코드안에

객체지향 특성을 모두 볼 수 있다.

{
  interface NormalCar {
    start(): void;
    stop(): void;
    run(): void;
    spec: Specifications;
    ownerName: string;
  }

  interface SuperCar {
    start(): void;
    stop(): void;
    run(): void;
    boost(): void;
    spec: Specifications;
    ownerName: string;
  }

  type Specifications = {
    tires: number;
    fuel: number;
    owner: string;
  };

  type OnOff = "off" | "on";

  class Vehicle {
    private engine: OnOff = "off";
    constructor(tires: number, fuel: number) {
      console.log(tires, fuel);
    }

    start = (): void => {
      console.log("start");
      this.engineToggle("on");
    };

    stop = (): void => {
      console.log("stop");
      this.engineToggle("off");
    };

    run = (): void => {
      console.log("keep going...");
    };

    boost = (): void => {
      console.log("speed up!!!");
    };

    get status() {
      return "car status : " + this.engine;
    }

    private engineToggle = (nextStatus: OnOff): void => {
      this.engine = nextStatus;
    };
  }

  class Car extends Vehicle {
    constructor(private tires: number, private fuel: number, private owner: string) {
      super(tires, fuel);
    }
    
    get spec() {
      return {
        tires: this.tires,
        fuel: this.fuel,
        owner: this.owner,
      };
    }
    
    get ownerName() {
      return this.owner;
    }
    
    set ownerName(newName: string) {
      this.owner = newName;
    }
  }

  // car 인스턴스 생성
  const myCar = new Car(4, 50, "steve");
  myCar.start();
  console.log(myCar.status);
  
  myCar.stop();
  console.log(myCar.status);

  // car 인스턴스 생성할 때 interface를 이용해 추상화
  const myFamilyCar: NormalCar = new Car(4, 60, "steve_family");
  // myFamilyCar.boost() <-- error

  const myHobbyCar: SuperCar = new Car(4, 48, "steve_hobby");
  myHobbyCar.boost();
}

1. 캡슐화

캡슐화는 ownerName을 통해 알수있다.

사용자는 ownerName을 읽거나 쓸수 있지만

class내에서는 owner라는 이름으로 존재한다.

 

사용자는 직접 owner를 수정하거나

읽어들일 수 없다.

 

마찬가지로 engine의 상태 또한

직접 변경할수 없으며,

시동을 걸거나(start) 끔으로써(stop)

상태를 변경할 수 있다.

 

이렇게 변수를 은닉함으로써

불필요하게 class내 멤버변수에 접근하여

값을 변경하는 경우를 방지할 수 있다.

 

2.  상속

Vehicle -> Car

위와 같은 방식으로 상속이 진행되었다.

 

Vehicle이라는 큰 카테고리 내에서 

공통의 속성이나 이름을 묶어주었다.

(모든 탈것에는 공통적으로  tires, fuel, start, stop, run 등의 기능이 존재함)

 

Car에는 Vehicle이 갖는 특성 외에

항상 주인이 정해지게 된다.

 

따라서, vehicle을 상속함과 동시에

생성자함수(constructor)에서 owner를 지정하게 해주었다.

 

다음, 제품별로 고유의 특성들을 또 갖게될텐대

porche 같은 고성능 차에는 boost라는 기능이 추가된다거나

주유구 방향 등을 설정할수도 있겠다.

 

3. 추상화

추상화에는 두가지 방법이 있다.

접근제한자 (private, public, protected)를 통한 추상화와

interface를 통한 추상화가 있다.

 

추상화의 목적은 사용성의 향상에 있다.

예를들어, 추상화처리를 하지 않았다면

Car를 통해 생성한 인스턴스를 사용하고자 할때

start,stop,run,boost,engine,owner,fuel,tires ...

등등 불필요한 properties를 보게될것이다.

 

개발자가 제공하고자하는 항목만을

추상화를 통해 제공할 수 있다.

 

위에서도 private으로 정보 접근을 제한함과 동시에

불필요한 옵션을 줄였고,

interface를 통해 Normal Car에서는 boost라는

property를 제공하지 않게 설정하였다.

 

3. 다형성

부모의 특성을 재정의하는

"오버라이딩"을 통해 이루어지게되는데,

자식들이 부모의 특성을 그대로 물려받기도 하지만

종종 변화를 주는 경우가 생긴다.

 

만약, 절차지향 프로그래밍이었다면

부분적으로 다르게 동작하는 자동차를 만들려면

대부분의 코드를 중복해서 사용하고

해당 부분을 수정해서 새로운 함수를 만들어야 할것이다.

 

객체지향 프로그래밍에서는

오버라이딩을 통해 원하는 부분만 수정한

새로운 객체를 쉽게 생성해낼수있다.

 

이를 다형성이라고 한다.

728x90
반응형
728x90

좀 더 나은 개발자가 되기위해

나를 되돌아보고

무엇을 다시 혹은 더 공부해야 될지

고민하던중

 

내가 OOP를 제대로 못하고있다고

생각이 들었다.

 

아직도 OOP를 제대로 못한다니

부끄럽지만 숨기지않고

인정하고 공부했을때

나아갈수 있는것이다.

 


아래 내용 관련 Github :)

https://github.com/jaekwangLee/oop_js_study

 

 

그러면 가장 기본적인

객체지향 프로그래밍의 의미를 알아본다.

 

시중에서 접하는 책들에서는

자동차, 커피머신, 로봇 등을 예시로 든다.

 

그 전에

더 이전부터 존재했던 개념인

절차지향 프로그래밍의 의미와

단점에 대해서 알아야

왜 객체지향이 등장했는지,

객체지향을 적용해야만 하는 이유를

알수있다.

 

절차지향 프로그래밍이 뭘까?

말그대로 절차를 중요시하며

순차적으로 실행되는 프로그래밍 기법을 말한다.

 

예를들어, 자동차가 있다면

시동 (start) 을 구현한다고 가정해보자.

절차지향적인 프로그래밍에서는 어떻게 할까?

// car.js
function car() {
  const fuel = 50;
  const brand = 'porche';
  const owner = 'steve';
  const tires = 4;
  
  function stop() {
    1. 사용자가 시동을 끈다.
    2. 액션을 엔진에 전달한다.
    3. 엔진이 신호를 받는다.
    4. 엔진이 동작을 멈춘다..
  }

  function start() {
    1. 사용자가 시동을 건다
    2. 액션을 엔진에 전달한다.
    3. 엔진이 신호를 받는다.
    4. 엔진이 동작을 시작한다.
  }
}
...

 (역시 개발자는 코드 구조로 봐야 이해가 쉽다)

 

이렇게 개발을 하면 단점이 뭘까?

무슨 문제가 있기에 객체지향을 해야하는 걸까?

 

1. 유지보수

누군가 저렇게 개발해둔 코드를 유지보수 하게 됐다면

지옥을 맛볼것이다.

시동 부분의 일부를 수정하는데도

이해해야 하는 코드가 너무나도 방대한것이다.

 

자동차 관련 코드 전체를 읽어야지

수정이 가능한 상황이 발생하기 쉽상이다.

 

2. 재사용성

start라는 시동 액션이 여기서만 쓰일까?

1) 개발중인 자동차와 다 똑같은데,

시동걸때 "시동!" 이라고 음성을 들려주는 자동차를 만들려면

수없이 중복되는 코드들을 짜야될것이다.

 

2) 자동차가 아닌 오토바이의 시동을 개발하게 되었다면,

자동차의 시동부와 액션이 같을 확률이 높지만 우리는

수없이 중복되는 코드들을 짜야될것이다.

 


 

이런 문제들을 알고

객체지향에 대해 다시 알아보자.

 

흔히 말하는 객체지향의 장점은

아래의 코드로 확인할 수 있다.

{
  interface NormalCar {
    start(): void;
    stop(): void;
    run(): void;
    spec: Specifications;
    ownerName: string;
  }

  interface SuperCar {
    start(): void;
    stop(): void;
    run(): void;
    boost(): void;
    spec: Specifications;
    ownerName: string;
  }

  type Specifications = {
    tires: number;
    fuel: number;
    owner: string;
  };

  type OnOff = "off" | "on";

  class Vehicle {
    private engine: OnOff = "off";
    constructor(tires: number, fuel: number) {
      console.log(tires, fuel);
    }

    start = (): void => {
      console.log("start");
      this.engineToggle("on");
    };

    stop = (): void => {
      console.log("stop");
      this.engineToggle("off");
    };

    run = (): void => {
      console.log("keep going...");
    };

    boost = (): void => {
      console.log("speed up!!!");
    };

    get status() {
      return "car status : " + this.engine;
    }

    private engineToggle = (nextStatus: OnOff): void => {
      this.engine = nextStatus;
    };
  }

  class Car extends Vehicle {
    constructor(private tires: number, private fuel: number, private owner: string) {
      super(tires, fuel);
    }
    
    get spec() {
      return {
        tires: this.tires,
        fuel: this.fuel,
        owner: this.owner,
      };
    }
    
    get ownerName() {
      return this.owner;
    }
    
    set ownerName(newName: string) {
      this.owner = newName;
    }
  }

  // car 인스턴스 생성
  const myCar = new Car(4, 50, "steve");
  myCar.start();
  console.log(myCar.status);
  
  myCar.stop();
  console.log(myCar.status);

  // car 인스턴스 생성할 때 interface를 이용해 추상화
  const myFamilyCar: NormalCar = new Car(4, 60, "steve_family");
  // myFamilyCar.boost() <-- error

  const myHobbyCar: SuperCar = new Car(4, 48, "steve_hobby");
  myHobbyCar.boost();
}

 

다음 장에서

위 코드에 대한 분석과

객체지향의 장점에 대해

이어서 공부해보도록 하자.

728x90
반응형
728x90

나는 그동안 redux를 쓰며 의문을 가졌다.

사실 쓰기 매우 꺼려지기까지 했다.

 

global state 관리를 위해

redux를 쓰는것은 동의

 

다만, 그 많은 코드를 써가면서까지

비동기 요청을 하고

응답된 데이터를 state로 관리하고

해야되는걸까?

 

실제로 서비스를 개발하면서

gloal state로 관리해야될 데이터는

많지 않았던것같다.

 

때에 따라서는

global state로 관리되는 데이터들에대한

최신화 문제를 겪기도했다.

 

이번에 새로운 앱 MVP 제작을위해

자료조사를 하던도중

이 포스팅을 보고

"react-query" 를 도입해보았다.

 

react-query는 global state를

server-state와 client-state라는

두개의 개념으로 분리해서 바라본다.

 

나는 이에 크게 동의했다.

 

예를들어, 포스팅 목록을 불러왔을때는

글로벌로 관리해야한다고본다.

(사용자가 새로고침하기 전까지는)

 

대신, 포스팅 단일 데이터는

굳이 글로벌로 관리할 필요가 있을까?

아니라고 생각한다.

 

global state라는 개념을 도입하게된 배경은

[부모 -> 자식 -> 자식 ->  자식 -> 자식]

으로 props를 전달하는데 불편함이 있어서이며,

불필요한 fetch를 방지하기 위함이다.

 

포스팅 단일 데이터호출 및 렌더링에는

이런 문제가 있지도 않을텐대

그 많은 코드를 써가며 global state로

관리할 이유가 없다고 생각하는것이다.

 

이런 측면에서 접근했을때

경우에 따라 global state로 관리해야 하는 경우에만

Redux를 이용하고

일반적으로는 state 그 자체면 충분하다.

 

특히나, 비동기 요청을 위해

redux-sage, redux-thunk등을 쓸 이유?

는 없어진다고 생각한다.

 

아직까지는 대규모 프로젝트에서

관리의 용이성을 위해 redux + redux-saga를 많이 쓴다곤 하지만

react-query가 어느정도 궤도에 이르면

redux + react-query 구조로 가지는 않을까하고

조심스럽게 예상해본다.

 

이제 만들고있는 앱에

redux도 붙이러 가야겠다

 


 

redux-saga, redux-thunk가

너무나도 싫어서

기피하고 외면하던 게으른 개발자가

 

마음에 쏙드는 신문물을 발견해

작성한 포스팅입니다.

728x90
반응형
728x90

pm2를 이용해서

앱서버를 백그라운드 환경으로 운영이 가능하다.

 

여기서 한발 더 나아가

cluster mode로 실행중인 node.js서버를

pm2를 이용해 돌리곤 했었다.

 

하지만 배포할때마다 일시적으로 중단되어버리는 서버.

원인은 restart로 배포하기때문.

 

reload 명령어를 통해

kill -> restart

reset 형식으로 서버 중단없이

무중단 배포가 가능하다.

pm2 reload index.js

 

근데 이를 제대로 활용하려면

몇가지 설정이 필요하다

 


 

pm2 ecosystem

 

위 명령어로 ecosystem.config.js 파일을 얻을수 있다.

다양한 설정들이 기본으로 세팅되어있는데

docs를 보면서 입맛에 맞게 설정하면 된다.

 

진짜 간단히는

module.exports = {
    apps: [
        {
            name: 'server',
            script: './index.js',
            watch: '.',
            instances: -1, // 클러스터 모드
            // exec_mode: 'cluster', <-- 이것도 클러스터 모드
        },
    ],
};

이렇게만 설정해도 된다

 

package.json을 수정하면

npm 명령어로 서버에서

간편히 실행 혹은 reload 하는것도 가능하다.

scripts: {
  "start": "pm2 start ./ecosystem.config.js --only server",
  "reload": "pm2 reload server"
}

 

고객에게 불편을 주지말자

 


여기서 더 나아간다면

실행중인 process가 덮어씌워지는 경우를

방지하고

reload할 수 있다.

 

// server.js

const app = express();


let disableKeepAlive = false;

// 중단이 감지되면 Keep 상태인 요청 닫음
app.use((req, res, next) => {
    if (disableKeepAlive) {
        res.set('Connection', 'close');
    }
    next();
});

const service = app.listen(process.env.PORT, () => {
    console.log(`The application is listening on port ${process.env.PORT}`);
    if (process.send) {
        console.log('send')
        process.send('ready');
    }
});

process.on('SIGINT', async () => {
    disableKeepAlive = true;
    service.close();
    process.exit(0);
});

이런식으로 구성해주면 된다.

728x90
반응형