현재 프로젝트를 하면서 소셜로그인 기능을 만드는 중이다. 구글 & 카카오 & 애플 로그인을 만들고 있으며 마지막에 개발하게 된 놈이 바로 애플.

 

애플 디벨로퍼 설정은 다음 블로그를 참고했다

 

https://dalgoodori.tistory.com/49

 

[Flutter] Apple Login ① - 준비

마지막으로 구현할 소셜로그인은 애플 로그인 입니다. 애플 로그인을 구현하기 위해서는 개발자 계정에 가입되어있어야하고 비용은 1년에 13만원 입니다. ㅠㅠ Apple Developer There’s never been a bette

dalgoodori.tistory.com

 

 

또한, 플러터에서 애플로그인하는 기능은 다음 패키지를 활용했다

 

https://pub.dev/packages/sign_in_with_apple

 

sign_in_with_apple | Flutter Package

Flutter bridge to initiate Sign in with Apple (on iOS, macOS, and Android). Includes support for keychain entries as well as signing in with an Apple ID.

pub.dev

 

설정방법도 꽤나 상세히 나와있으니, 플러터로 애플로그인 구현하시는 분들은 저 2개 링크타고 가서 보면 될듯

 


암튼 뭐 그렇게 해서 플러터에서 애플로그인하는 기능은 만들었다. 구글 & 카카오 로그인 만들 때는 access token을 바로 플러터 단에서 얻게 되는 방식이었는데, 애플은 access token이 아니라 authorization codeidentity token이란 걸 준다.

 

나는 현재 프론트 단에서 access token을 받아서 그걸 백엔드로 넘긴 뒤 백엔드에서 해당 토큰을 통해 유저 정보를 조회하는 방식으로 만들고 있었기 때문에, 발급받은 identity token으로 사용자 정보를 조회할 수 있는 엔드포인트가 있나를 먼저 살폈다(구글, 카카오는 access token 통해서 사용자 정보 조회하는 엔드포인트가 있기 때문..) 그러나! 애플은 그런 엔드포인트가 없었다.. 애플 개발자문서에도 지들은 그런 엔드포인트 없다고 말하고 있음. ㅋㅋ

 

그 대신에 처음에 로그인 성공했을 때 authorization code랑 identity token말고도 로그인한 유저의 정보(이메일 등)을 주긴 준다. 이 정보를 백엔드로 보낸다면? 이란 생각이 들었으나, 바로 철회했다. 지금 플젝에서 어차피 보내게 될 정보야 끽해야 이메일인데, 사실상 이메일과 플랫폼명(애플, 카카오 등)을 받아서 로그인해주는 api를 만드는 건 보안상 매우 위험하기 때문. 공격자들이 내 이메일을 안다면, 지들도 로그인하는 엔드포인트로 내 이메일이랑 플랫폼명만 보내면 로그인된다면 그 서비스를 누가 이용하겠는가. 사실 구글, 카카오 로그인 만들 때도 같은 이유로 프론트 단에서 회원정보가 아니라 플랫폼들로부터 받은 access token을 넘기게 했던 거고.

 

그러면 어떻게 해야 할까? 애플로부터 발급받은 identity token에는 공급자, 대상자, 유효시간, 사용자 이메일과 같은 정보들도 들어있다. 따라서 identity token을 백엔드로 보내고 백엔드에서 사용자 이메일을 꺼내서 사용해야겠다는 생각이 들었다.

 

 

전달해주는건 쉽지. 그러면 어떻게 꺼내야 하는가? identity token은 일단 jwt형식이어서, 이 토큰을 만들 때 사용했던 키를 몰라도 디코딩해서 내용을 볼 수 있다. 애플이 지들 키 써서 만든 토큰인데 당연히 내가 그 키를 알 방법은 없고, 직접 디코딩해서 보는 식으로 해야겠군! 이라는 생각이 들었다. split메서드와 Base64 decoder를 이용해 디코딩해서 금방 email값을 뽑아낼 수 있었다.

 

근데 이 방법, 생각해보니 보안적으로 위험하다. 우선 구글과 카카오에서 썼던 "토큰을 동봉해 유저정보를 조회하는 엔드포인트로 요청을 보내서 받는 방식"은 내부적으로 그 토큰이 유효한 토큰인지 검증해줄 것이다. 근데 지금 내가 애플에서 온 토큰을 직접 까는 이 방식은 요 토큰이 유효한 토큰인지, 즉 제대로 된 토큰인지 검증하지 않는다. 한마디로 악의적 사용자가 대충 내 이메일을 알아낸다음 내 이메일을 jwt로 만들어서 우리 백엔드의 애플로그인 엔드포인트로 요청을 보내면 로그인이 되는 말도 안 되는 상황이 연출될 수 있다.

 

한마디로, 넘겨받은 identity token이 합법적인 토큰인지 검증할 필요가 있다. 찾아보니 공식 문서에 해당 내용에 대한 글이 있었다. 다행쓰..

 

애플은 다음과 같이 5가지 스텝을 통해 identity token을 우리가 알아서(?) 검증하라고 한다

 

https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user

 

애플의 공개키로 identity token의 서명을 확인하라는 내용을 볼 수 있다. 아니 근데 애플 양반 당신들이 쓰는 키를 내가 어떻게 알아!라고 말하고 싶지만 보다시피 공개키다 공개키. 

 

애플은 다음 링크에 있는 엔드포인트를 통해 자기들이 현재 시점에서 사용중인 키들의 정보를 제공한다. 허허허..

 

https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature

 

Fetch Apple’s public key for verifying token signature | Apple Developer Documentation

Fetch Apple’s public key to verify the ID token signature.

developer.apple.com

 

다음과 같은 형식일 것이다.

 

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "YuyXoY",
      "use": "sig",
      "alg": "RS256",
      "n": "1JiU4l3YCeT4o0gVmxGTEK1IXR-Ghdg5Bzka12tzmtdCxU00ChH66aV-4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6_VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU-HKabsKOFdD4VQ5PCXifjpN9R-1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI_I1j_jbN3gflK_8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A_JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL-Swk5XNGbWZdTROmaTNzJhV-lWT0gGm6V1qWAK2qOZoIDa_3Ud0Gw",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "fh6Bs8C",
      "use": "sig",
      "alg": "RS256",
      "n": "u704gotMSZc6CSSVNCZ1d0S9dZKwO2BVzfdTKYz8wSNm7R_KIufOQf3ru7Pph1FjW6gQ8zgvhnv4IebkGWsZJlodduTC7c0sRb5PZpEyM6PtO8FPHowaracJJsK1f6_rSLstLdWbSDXeSq7vBvDu3Q31RaoV_0YlEzQwPsbCvD45oVy5Vo5oBePUm4cqi6T3cZ-10gr9QJCVwvx7KiQsttp0kUkHM94PlxbG_HAWlEZjvAlxfEDc-_xZQwC6fVjfazs3j1b2DZWsGmBRdx1snO75nM7hpyRRQB4jVejW9TuZDtPtsNadXTr9I5NjxPdIYMORj9XKEh44Z73yfv0gtw",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "W6WcOKB",
      "use": "sig",
      "alg": "RS256",
      "n": "2Zc5d0-zkZ5AKmtYTvxHc3vRc41YfbklflxG9SWsg5qXUxvfgpktGAcxXLFAd9Uglzow9ezvmTGce5d3DhAYKwHAEPT9hbaMDj7DfmEwuNO8UahfnBkBXsCoUaL3QITF5_DAPsZroTqs7tkQQZ7qPkQXCSu2aosgOJmaoKQgwcOdjD0D49ne2B_dkxBcNCcJT9pTSWJ8NfGycjWAQsvC8CGstH8oKwhC5raDcc2IGXMOQC7Qr75d6J5Q24CePHj_JD7zjbwYy9KNH8wyr829eO_G4OEUW50FAN6HKtvjhJIguMl_1BLZ93z2KJyxExiNTZBUBQbbgCNBfzTv7JrxMw",
      "e": "AQAB"
    }
  ]
}

 

즉 정상적인 경우라면, 내가 아까 발급받은 identity token은 위에 있는 키 후보들(?) 중 하나로 만들어진 셈이다. 즉 위 후보들 중 실제로 내가 받은 identity token을 만들 때 써진 키를 찾아야 한다.

 

근데 어떻게 찾느냐? identity token은 아까 말했듯 jwt다. jwt의 header 부분에는 어떤 알고리즘을 썼는지에 대한 정보들이 있는데, identity token도 그와 마찬가지로 헤더에 관련 정보들이 있다. 구체적으로는 alg값과 kid값이 들어있다.

 

근데 위에 있는 후보 키들을 보면, 각각 kid값과 alg값을 갖고 있는 걸 볼 수 있다. 즉, identity token의 header에 들어있는 alg & kid와 동일한 값을 갖는 후보 키가 identity token을 만들 때 쓰인 키다. 

 

말은 쉽지~ 코드를 보여줘! 난 다음과 같이 구현했다. 전체 코드가 아닌 메서드들만..허헣

 

    // identity token의 헤더에서 alg, kid 추출해서 저장하는 메서드
    private Map<String, String> getAlgAndKidFromIdToken(String idToken) throws ParseException {
        Map<String, String> algAndKid = new HashMap<>();

        String header = idToken.split("\\.")[0];
        Base64.Decoder decoder = Base64.getUrlDecoder();
        JSONObject headerContent = (JSONObject) jsonParser.parse(
                new String(decoder.decode(header))
        );

        algAndKid.put("alg", (String) headerContent.get("alg"));
        algAndKid.put("kid", (String) headerContent.get("kid"));

        return algAndKid;
    }

    // 애플의 공개키들을 받는 엔드포인트로부터 키 후보들을 가져오는 메서드
    private JSONArray getAvailablePublicKeyObjects() throws ParseException {
        HttpEntity<String> httpEntity = new HttpEntity<>(new HttpHeaders());
        ResponseEntity<String> res = restTemplate.exchange(
                APPLE_PUBLIC_KEY_URL, HttpMethod.GET, httpEntity, String.class);
        JSONObject availablePublicKeysContent = (JSONObject) jsonParser.parse(res.getBody());
        return (JSONArray) availablePublicKeysContent.get("keys");
    }

    // 키 후보들과 identity token에서 뽑아낸 alg, kid를 비교해 매칭되는 키를 찾는 메서드
    private JSONObject findMatchedPublicKeyObj(JSONArray availablePublicKeyObjects, String alg, String kid) {
        if (availablePublicKeyObjects == null || availablePublicKeyObjects.size() == 0) {
            throw new AppleKeyInfoNotReceivedException();
        }

        for (JSONObject keyObj : (Iterable<JSONObject>) availablePublicKeyObjects) {
            String algFromKey = (String) keyObj.get("alg");
            String kidFromKey = (String) keyObj.get("kid");

            if (Objects.equals(algFromKey, alg) && Objects.equals(kidFromKey, kid)) {
                return keyObj;
            }
        }

        return null;
    }
    
    // 찾아낸 후보 키(jsonObject)를 공개키 인스턴스로 만드는 메서드
    private PublicKey generatePublicKey(JSONObject applePublicKeyObj) {
        if (applePublicKeyObj == null) {
            throw new MatchedKeyNotFoundException();
        }

        String kty = (String) applePublicKeyObj.get("kty");
        byte[] modulusBytes = Base64.getUrlDecoder().decode((String) applePublicKeyObj.get("n"));
        byte[] exponentBytes = Base64.getUrlDecoder().decode((String) applePublicKeyObj.get("e"));

        RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(
                new BigInteger(1, modulusBytes),
                new BigInteger(1, exponentBytes)
        );

        try {
            KeyFactory keyFactory = KeyFactory.getInstance(kty);
             return keyFactory.generatePublic(publicKeySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new ApplePublicKeyNotGenerateException();
        }
    }

 

이렇게 만든 키를 활용해, identity token의 서명을 확인할 수 있을 것이다. 한 마디로 이 키로 합법적으로 토큰을 깔 때(?) 오류가 안 나야 한다는 말.

 

그 뒤에 nonce검증이 있는데, 이건 패쓰.. nonce값이 토큰에 안 담겨있어서,,허허

 

iss, aud 검증은 다음과 같이 했다. 단순히 equal 비교

 

    public void verifyAppleIdTokenClaim(Claims claims) {
        if (!claims.getIssuer().equals(issuer)) {
            throw new IssuerNotMatchedException();
        }

        if (!claims.getAudience().equals(clientId)) {
            throw new ClientIdNotMatchedException();
        }
    }

 

유효기간 만료의 경우 이미 키를 통해 claims를 얻는 과정이 잘 통과되면 검증된 것과 마찬가지여서 굳이 넣지는 않았다. 필요하다면 claims의 getExpiration과 before같은 걸 조합해서 만들 수 있을 것이다.

 

후..삽질하다가 너무 어이없게 해결해서 쓴다. 사실 내가 무지했던 거지만..


플러터에서 dio를 통해 스프링부트에서 간단하게 만든 api를 테스트하는 중이었다. 근데 어머나?

SocketException: Connection refused (OS Error: Connection refused, errno = 111)

라는 오류가 떴다. 

 

포스트맨으로 할 때도 됐고,,웹에서 할 때도 됐는데,,왜 안 될까.. CORS문제인가하면서 이상한 뻘짓들을 했는데

답은 너무나도 간단했다.

 

안드로이드 에뮬레이터는 localhost를 사용할 수 없는 거였다

 

하..너무 당연하게 웹에서 구동하는 줄 알았따 ㅜㅠ 이거 땜에 뭔 고생을 한건지..

 

암튼, localhost대신 10.0.2.2를 쓰면 된다고 한다. 물론 프로덕션으로 가게 되면 다른 걸 설정해야됨!

 

https://developer.android.com/studio/run/emulator-networking?hl=ko 

 

Android Emulator 네트워킹 설정  |  Android 스튜디오  |  Android Developers

에뮬레이터는 앱에 복잡한 모델링 및 테스트 환경을 설정하는 데 사용할 수 있는 다목적 네트워킹 기능을 제공합니다.

developer.android.com

 

CI란?

Continuous Intergration의 줄임말로, 지속적인 통합을 의미한다. CD(Continuous Delivery&Deployment)와 짝꿍 관계기도 하다. 

 

그럼 지속적 통합은 뭘까? 우선 통합의 의미를 살펴보자. 나 또는 다른 사람이 어떠한 코드 변경(새로운 기능 추가, 수정, 삭제 등)을 했을 때 그 코드가  빌드 및 테스트되어 우리의 공유 리포지토리(깃헙 등)에 병합되는 걸 통합이라 부른다. 지속적 통합이란, 이 과정이 정기적으로 계속해서 일어난다는 것을 의미하는 것이다.

 

이는 다수의 개발자가 다 함께 코드 작업을 할 경우, 서로 충돌이 일어날 수 있는 문제를 해결하기 위해 도입된 개념이라고 한다. 통합 과정을 주기적하게 되니, 자연스럽게 충돌 과정이 최소화된다는 거다. CI는 다시 말해 빌드 및 테스트를 자동으로 실시하여 공유 리포지토리에 통합하는 과정이라고 이해할 수 있으며, 이를 통해 코드 변경 내용을 우리 손을 거치지 않고 자동으로 빌드하고 테스트할 수 있다.

 

 

GitHub Action이란?

GitHub에서 제공하는 서비스로, CI & CD 플랫폼이다. 리포지토리에 .github폴더를 만들고, 그 안에 workflows폴더를 만든 뒤 그 안에 yaml파일을 만드는 것으로 구축할 수 있다. 그러면 어떤 동작이 발생했을때, yaml파일에 내가 작성했던 workflow가 실행되게 할 수 있다.

 

 

플러터 프로젝트 CI 구축

GitHub marketplace에 flutter란 이름으로 검색하면, Flutter Action라는 이름의 Action이 있다

 

https://github.com/marketplace/actions/flutter-action

 

Flutter action - GitHub Marketplace

Setup your runner with Flutter environment

github.com

 

친절하게 사용법이 나와있어서 따라하면 된다.

난 우분투, 윈도우, 맥 모두에서 테스트 & 빌드를 해볼 수 있게끔 만들었다.

 

name: Flutter CI (test & build)

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]
    
jobs:
  Tests-and-build-ubuntu-and-window:
    name: Tests and Build on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v2
        with:
          distribution: 'zulu'
          java-version: '11'
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.10.0'
          channel: 'stable'
      - run: flutter pub get
      - run: flutter test
      - run: flutter build apk
      - run: flutter build appbundle

  Tests-and-build-macos:
    name: Tests and Build on MacOS
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v3
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.10.0'
        channel: 'stable'
        architecture: x64
    - run: flutter pub get
    - run: flutter test
    - run: flutter build ios --release --no-codesign

 

 

uses : 다른 사람들이 이미 만든 action을 이용하는 것

run : 직접 커맨드를 실행하는 것

 

이라고 이해하면 된다.

 

[ runs-on ] 작업들이 실행될 인스턴스 환경(OS)

[ uses: actions/checkout@v3 ] 작업한 깃헙 레포지토리의 소스코드 가져오기(일단 코드를 가져와야 테스트하든 빌드하든 하니까)

[ uses: actions/flutter-action@v2 ] 플러터 SDK 설치. with로 버전명, 채널을 설정 가능

[ run : flutter pub get ] 체크아웃으로 가져온 프로젝트가 사용하는 패키지들 설치

 

 

 

음..사실 yaml파일을 내가 한 것보다 더 리팩토링이 가능했다. if문 등을 통해 더 줄일 수 있겠다는 생각이 들었지만 GitHub Action을 처음 하던지라 일단 동작하게끔 만들자는 생각으로 macos에서 실행되는 건 따로 분리를 했다. 우리의 킹갓지피티에게 물어보니 더 줄일 수 있는 것 같다.

 

CI/CD를 구축해본 적이 없어 GitHub Action 공부를 많이 해야 할 것 같아 좀 걱정이었는데 막상 해보니까 되게 간단하다는 생각이 든다. (물론 CD는 구축하지도 않았지만ㅋㅋ) 역시 뭐든 직접 해봐야 더 잘 알게 되는 것 같다.

스플래시 스크린이란?

애플리케이션 실행 시 화면 컨텐츠가 로딩되기까지 일시적으로 보여주는 화면을 말한다.


그럼 이걸 플러터에선 어떻게 만들 수 있는가?

찾아보니까 다른 방법들도 있었지만, 가장 쉬운 방법은 flutter_native_splash라는 패키지를 이용하는 것이었다.

 

매우 쉽다. 아래 링크에 나온 방법대로 패키지를 설치하고 사용하면 된다. 

https://pub.dev/packages/flutter_native_splash

 

flutter_native_splash | Flutter Package

Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.

pub.dev

 

패키지 설치 후,  flutter_native_splash.yaml을 만들어주고 다음 내용을 복붙해준다. 위 웹페이지에 있는 내용과 동일하다.

 

flutter_native_splash:
  # This package generates native code to customize Flutter's default white native splash screen
  # with background color and splash image.
  # Customize the parameters below, and run the following command in the terminal:
  # dart run flutter_native_splash:create
  # To restore Flutter's default white splash screen, run the following command in the terminal:
  # dart run flutter_native_splash:remove

  # color or background_image is the only required parameter.  Use color to set the background
  # of your splash screen to a solid color.  Use background_image to set the background of your
  # splash screen to a png image.  This is useful for gradients. The image will be stretch to the
  # size of the app. Only one parameter can be used, color and background_image cannot both be set.
  color: "#42a5f5"
  #background_image: "assets/background.png"

  # Optional parameters are listed below.  To enable a parameter, uncomment the line by removing
  # the leading # character.

  # The image parameter allows you to specify an image used in the splash screen.  It must be a
  # png file and should be sized for 4x pixel density.
  #image: assets/splash.png

  # The branding property allows you to specify an image used as branding in the splash screen.
  # It must be a png file. It is supported for Android, iOS and the Web.  For Android 12,
  # see the Android 12 section below.
  #branding: assets/dart.png

  # To position the branding image at the bottom of the screen you can use bottom, bottomRight,
  # and bottomLeft. The default values is bottom if not specified or specified something else.
  #branding_mode: bottom

  # The color_dark, background_image_dark, image_dark, branding_dark are parameters that set the background
  # and image when the device is in dark mode. If they are not specified, the app will use the
  # parameters from above. If the image_dark parameter is specified, color_dark or
  # background_image_dark must be specified.  color_dark and background_image_dark cannot both be
  # set.
  #color_dark: "#042a49"
  #background_image_dark: "assets/dark-background.png"
  #image_dark: assets/splash-invert.png
  #branding_dark: assets/dart_dark.png

  # Android 12 handles the splash screen differently than previous versions.  Please visit
  # https://developer.android.com/guide/topics/ui/splash-screen
  # Following are Android 12 specific parameter.
  android_12:
    # The image parameter sets the splash screen icon image.  If this parameter is not specified,
    # the app's launcher icon will be used instead.
    # Please note that the splash screen will be clipped to a circle on the center of the screen.
    # App icon with an icon background: This should be 960×960 pixels, and fit within a circle
    # 640 pixels in diameter.
    # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle
    # 768 pixels in diameter.
    #image: assets/android12splash.png

    # Splash screen background color.
    #color: "#42a5f5"

    # App icon background color.
    #icon_background_color: "#111111"

    # The branding property allows you to specify an image used as branding in the splash screen.
    #branding: assets/dart.png

    # The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that
    # apply when the device is in dark mode. If they are not specified, the app will use the
    # parameters from above.
    #image_dark: assets/android12splash-invert.png
    #color_dark: "#042a49"
    #icon_background_color_dark: "#eeeeee"

  # The android, ios and web parameters can be used to disable generating a splash screen on a given
  # platform.
  #android: false
  #ios: false
  #web: false

  # Platform specific images can be specified with the following parameters, which will override
  # the respective parameter.  You may specify all, selected, or none of these parameters:
  #color_android: "#42a5f5"
  #color_dark_android: "#042a49"
  #color_ios: "#42a5f5"
  #color_dark_ios: "#042a49"
  #color_web: "#42a5f5"
  #color_dark_web: "#042a49"
  #image_android: assets/splash-android.png
  #image_dark_android: assets/splash-invert-android.png
  #image_ios: assets/splash-ios.png
  #image_dark_ios: assets/splash-invert-ios.png
  #image_web: assets/splash-web.gif
  #image_dark_web: assets/splash-invert-web.gif
  #background_image_android: "assets/background-android.png"
  #background_image_dark_android: "assets/dark-background-android.png"
  #background_image_ios: "assets/background-ios.png"
  #background_image_dark_ios: "assets/dark-background-ios.png"
  #background_image_web: "assets/background-web.png"
  #background_image_dark_web: "assets/dark-background-web.png"
  #branding_android: assets/brand-android.png
  #branding_dark_android: assets/dart_dark-android.png
  #branding_ios: assets/brand-ios.gif
  #branding_dark_ios: assets/dart_dark-ios.gif

  # The position of the splash image can be set with android_gravity, ios_content_mode, and
  # web_image_mode parameters.  All default to center.
  #
  # android_gravity can be one of the following Android Gravity (see
  # https://developer.android.com/reference/android/view/Gravity): bottom, center,
  # center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
  # fill_vertical, left, right, start, or top.
  #android_gravity: center
  #
  # ios_content_mode can be one of the following iOS UIView.ContentMode (see
  # https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
  # scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
  # bottomLeft, or bottomRight.
  #ios_content_mode: center
  #
  # web_image_mode can be one of the following modes: center, contain, stretch, and cover.
  #web_image_mode: center

  # The screen orientation can be set in Android with the android_screen_orientation parameter.
  # Valid parameters can be found here:
  # https://developer.android.com/guide/topics/manifest/activity-element#screen
  #android_screen_orientation: sensorLandscape

  # To hide the notification bar, use the fullscreen parameter.  Has no effect in web since web
  # has no notification bar.  Defaults to false.
  # NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
  #       To show the notification bar, add the following code to your Flutter app:
  #       WidgetsFlutterBinding.ensureInitialized();
  #       SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top], );
  #fullscreen: true

  # If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
  # with the info_plist_files parameter.  Remove only the # characters in the three lines below,
  # do not remove any spaces:
  #info_plist_files:
  #  - 'ios/Runner/Info-Debug.plist'
  #  - 'ios/Runner/Info-Release.plist'

 

background image로 쓸 이미지가 있으면 주석 풀어서 경로 써주면 되고.. 뭐 그런 식이다

참고로 안드로이드 12 이상을 쓸 경우 android 12: 쪽에 있는 내용도 별도로 주석풀어서 작업해줘야 한다!! 이거 안 하면 안드로이드 쪽에선 에러난다.

 

또한 위 웹페이지에서 살펴볼 수 있듯, 안드로이드 12 이상을 쓰는 경우에는 권장하는 이미지 사이즈가 있다. 그거에 맞춰서 이미지 만들면 됨. 대략 가로 세로 1152px이어야 하고, 가운데에 지름이 768px인 원을 두고 그 안에 다 보여줄 수 있게끔 이미지를 만들어야 한다고 함.

 

그 뒤 터미널에 다음 커맨드를 입력하면 된다

dart run flutter_native_splash:create

 

그럼 알아서 배율에 맞게끔 이미지들이 생성되며(스플래시 스크린에서 보여줄 이미지들이 있는 경우) 스플래시 스크린이 만들어진다.

 

참고로, 수정이 필요한 경우 

flutter pub run flutter_native_splash:remove

 

요 커맨드 입력하고 내용 수정한 후, 다시 dart run ~ 이거 하면 된다.

+ Recent posts