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


플러터에서 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

 

등장배경

어쩌다보니 꽤나 잘 그린 것 같은 그림이 나왔다

우리가 만든 서비스가 유저를 대신해서 구글에서 제공하는 서비스에 뭔가를 하고 싶은 일들이 생겼던 거다. 가령 구글 캘린더에 일정등록을 우리가 만든 서비스가 해준다든가, 등등.. 이를 위해서는 유저로부터 그가 사용하는 구글에 대해 접근할 수 있다는 허락을 받아야 한다.

 

가장 쉬운 방법은 당연히 유저로부터 구글ID와 PW를 받는 것. 우리가 만든 서비스가 유저가 준 ID, PW를 기억하면서 적재적소에 써먹으면 된다. 상당히 쉽고 강력한 방법이다.

하지만 당연히 이걸 실제로 써먹을 수는 없다. 유저입장에선 우리가 만든 서비스를 신뢰할 수 없을 것이며, 구글 입장에서도 유저가 아니라 제 3자인 우리 앱이 유저의 ID와 PW를 가지게 되니 여간 골치아픈게 아니다. 결국 보안적으로 가당치도 않은 상황이 된다.

 

이런 문제에 대한 해결책으로 등장한 것이 본 포스트에서 다룰 OAuth다.

 

 

OAuth?

Open Authorization의 줄임말. 사용자(user)와 구글과 같은 플랫폼 사이에서 제 3자에 해당하는 우리의 서비스가 해당 플랫폼에 있는 사용자의 데이터에 접근할 수 있는 권한을 위임받을 수 있는 표준 프로토콜이다. 이 프로토콜을 통해 사용자는 우리가 만드는 서비스에 ID, PW를 맡길(?) 필요가 없고, 구글 등에 있는 사용자의 데이터에 대해 접근할 수 있는 권한을 우리 서비스가 부여받을 수 있게 된다.

 

 

OAuth의 원리

한 문장으로 요약하자면

 

사용자의 요청을 통해 구글이 access token을 발급해주고, 그 토큰을 통해서 우리가 구글에 존재하는 사용자의 데이터에 접근이 가능해지는 것

 

이다. 이제 이 페이지를 닫으셔도 됩니다

좀만 더 원리를 디테일하게 설명하기 전, 용어 정리를 한 번 하고 가야 한다.

 

 

Resouce Owner

우리가 만든 서비스를 이용하면서 구글 등에 데이터를 가지고 있는 사람. 즉 사용자를 말한다

 

Resouce Server

구글과 같이 사용자의 리소스를 가지고 있는 서버, 즉 우리가 만든 서비스가 제어하고자 하는 리소스를 가지고 있는 애를 말한다. 인증 관련된 서버와 자원 관련된 서버로 구분하기 위해 Authorization Server와 Resource Server 2개로 분리하기도 하는데, 본 포스트에선 Resource Server 하나로 뭉탕치도록(?) 하겠다. 

 

Client

Resource Server의 리소스를 이용하고자 하는 서비스. 즉 우리가 만든 서비스를 말한다

 

 

그럼 이제 본격적으로 OAuth의 동작순서 및 원리에 대해 좀 더 알아보자.

 


동작순서는 다음과 같다

 

  1. Resource Owner(사용자)가 Client(우리가 만든 서비스)의 [구글 계정으로 로그인] 과 같은 버튼을 누른다
  2. Client는 이를 접수(?)하고 Resource Server(구글 등)에게 전달
  3. Resource Server는 Resource Owner에게 로그인 페이지를 보여주고, Resource Owner가 로그인한다
  4. Resouce Server는 인증이 성공되면 Resource Owner에게 Client가 특정 리소스에 접근해도 되냐는 질의를 한다
  5. Resouce Owner가 허락한다면, Resouce Owner가 Authorization code를 Resource Owner에게 전달하면서 Resource Owner를 사전에 약속(Client와 Resource Server가 사전에 약속한 것임)된 Redirect URI로 리다이렉트시킴 (Authorization code: 일종의 임시 암호)
  6. 이를 통해 Client도 Resouce Owner가 Resource Server로부터 전달받은 Authorization code를 알게 됨
  7. Client는 사전에 Resource Server와 합의해서 가지고 있던 client secret이란 걸 가지고 있음. 이걸 Authorization code와 함께 Resource Server에게 전달.
  8. Resoruce Server가 이에 대한 인증이 끝나면, Client에게 access token(허가증)을 발급!
  9. 이후 Client는 Resource Server에 존재하는 Resource Owner의 리소스에 접근할 때는 아까 받았던 access token을 활용

 

그럼 각 단계를 좀 더 뜯어보자. 그 전에, 위 순서에서 보면 사전에 약속된, 합의된 이런 말이 나온다. 그것도 포함해서 각 단계를 뜯어보자.

 


0. 일단 우리가 만든 서비스를 등록

우선 Client, 즉 우리가 만드는 서비스가 구글 즉 Resource Server를 이용하기 위해선 Resource Server에 우리가 널 쓸거라고 사전에 등록을 해야 한다. 이 방법은 구글, 카카오, 애플 등 플랫폼별로 조금씩 다르다.

 

플랫폼 별로 방법이야 당연히 다른데 공통적으로 수행하는 작업이 있다. 바로 Redirection URI를 등록하는 것! 이 URI는 구글과 같은 플랫폼이 인증이 성공한 사용자(구글로 로그인을 눌러서 자신의 구글 계정으로 로그인한..위 순서에서 3 ~ 5번 참조)를 리다이렉트 즉 이동시킬 URI다. 위 순서에서 알 수 있듯, 이는 Resource Server로부터 Authorization code를 받은 Resource Owner가 오게 되는 URI다. (CallBack URL로도 부르는 듯)

 

(음 내가 이해한 대로 설명하자면..유저한테 "유저야, 우리 서비스를 통해 구글에 접근하고 싶지? 그럼 너가 구글에 들러서 걔네한테 받은 임시 허가증을 우리 집 창문으로 들고 와!" 라고 하는 상황이다. 구글이 Resource Server고, 임시 허가증이 Authorization Code다. 그리고 우리 집 창문이 Redirect URI, 즉 사용자가 구글에서 임시 허가증을 받은 뒤 와야 하는 "지정된 장소"인 것. 근데 구글이 친절하게도 직접 택시를 태워서 사용자를 우리 집 창문 앞으로 보내주는 것, 즉 리다이렉트 시켜주는 거다)

 

암튼 이렇게 등록이 끝나면 Client Id와 Client Secret(위 순서에서 7번을 참조)를 발급받는다

 

  • Client Id : 등록된 우리 서비스를 Resource Server가 식별할 수 있는 식별자
  • Client Secret : Client Id에 대한 비밀번호. 외부에 노출되면 절대 안 된다

 

즉 이런 등록과정, 즉 사전협의를 통해 client와 resource server는 client id 및 client secret, 그리고 redirect uri를 아는 상태에서 시작한다.

 


1. Resource Owner가 Client의 [구글 계정으로 로그인] 과 같은 버튼을 누른다

걍 이거 말하는 거임

 

ChatGPT 로그인 화면

 

여기서 구글로 로그인 이런걸 유저가 누른다는 말!

 


2. Client가 이를 Resource Server로 전달

이때 전달하는 주소는 다음과 같은 형식이다 (물론 플랫폼 별로 조금씩 차이가 있을 수도..?)

 

https://resource.server/?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}

 

아까 사전 협의를 통해 Client가 Client Id와 Redirect URI를 알고 있음을 상기하자. scope는 client가 resource server로부터 인가받을 권한의 범위를 말한다고 생각하면 된다(구글의 모든 리소스에 접근할 수 있는 것보단 딱 필요한 것에만 접근할 수 있게끔 하는 것이 당연히 좋다)

 


3. Resource Server가 로그인 페이지를 보여주고 Resource Owner가 로그인한다

이거 말하는 거임

 


4. Resource Server는 인증이 성공하면 Resource Owner에게 Client가 특정 리소스들에 접근해도 되냐는 질의를 함

인증이 성공(즉 Resource Owner가 로그인에 성공하면)하면, Resource Server는 쿼리스트링 형식으로 넘어온 파라미터들을 보며 Client가 본인이 아는 그 놈이 맞는지 검사한다(즉 사전에 협의된 녀석이 맞는지 검사). Resource Server 역시 Client Id와 Redirect URI를 알고 있음을 다시 한 번 상기하자.

 

검사하는 내용은,

 

  1. 파라미터로 전달된 client id값과 동일한 값을 내가(Resource Server가) 가지고 있는가?
  2. 가지고 있다면 그에 대한 redirect uri가 파라미터로 전달된 redirect uri와 동일한가?

 

이렇게 검사한 후에, 특정 리소스들에 접근해도 되냐는 질의를 하는데 그건 이런 거 말하는 거임

 

 


5. Resource Owner가 허락한다면, Resource Server는 Authorization code를 Resource Owner에게 전달하며 Redirect URI로 리다이렉트시킨다

특정 리소스들에 대한 접근 질의에 Resource Owner가 허락한다면, Resource Server는 해당 Client Id에 대해 특정 user_id를 갖는 Resource Owner가 특정 scope에 대한 행동을 허가했다는 사실을 기록한다. (즉 특정 유저가 우리 서비스에 대해 A, B라는 행동을 해도 된다고 허락했다는 것을 기억하는 것)

 

 

그 후, Resource Owner에게 Authorization code라는 임시 암호를 발급해주면서 어떤 uesr에게 해당 Authorization code를 발급했는지 기록한다. 그와 동시에, 사전에 합의했던 Redirect URI로 Resource Owner를 리다이렉트시킨다.

 


6. 이를 통해 Client도 Resource Owner가 Resource Server로부터 발급받은 Authorization Code를 알게 됨

별도의 부연설명은 생략

 

 


7. Client는 사전에 합의한 후 받았던 Client Secret과 함께 Authorization code를 Resource Server에게 전달

 

 

이 때 다음과 같은 형식으로 보내게 된다

 

https://resource.server/token?
	grant_type=authorization_code&
	code={authorization_code}&
	redirection_uri={redirect_uri}&
	client_id={client_id}&
	client_secret={client_secret}

 

각각에 대해 설명하자면

 

  • grant_type : 항상 "authorization_code"라는 문자열로 설정
  • code : 전달받은 authorization code를 넣으면 됨
  • redrection_uri : 사전에 합의한 바로 그 redirection_uri 넣으면 됨
  • client_id : 사전에 합의하고 받은 바로 그 client id 넣으면 됨
  • client_secret : 사전에 합의하고 받은 바로 그 client secret 넣으면 됨

 


8. Resource Server가 이에 대한 인증이 끝나면, Client에게 access token을 발급함

Resource Server는 Client에게 전달받은 code(= authorization code)값과 자신이 아까 기록한(5번 참조) Authorization code를 대조하며 인증을 함. 이 과정이 성공적으로 끝나면, Resource Server는 아까 자신이 기록했던 Authorization code를 지우고 Client에게 Access Token을 발급하며 해당 토큰을 어떤 user_id에게 발급했는지를 기록한다.

 

 


9. 이후 Client는 발급받은 Access token을 이용해 활용

자세한 설명은 생략한다.

 

 


참고로, Refresh token을 발급해주기도 한다고 한다. Access token이 만료되면 Refresh token을 통해서 Access token을 재발급받는 것.

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 ~ 이거 하면 된다.

형제간 margin병합 현상

형제간 margin충돌이 일어날 때, 각각의 margin값들이 반영되는게 아니라 더 큰 margin으로 병합되는 현상.

음 말로 적으니까 뭔 말인지 어렵다..

 

  <div id="box1"></div>
  <div id="box2"></div>
    #box1 {
      width: 100%;
      height: 200px;
      background-color: yellow;
      margin-bottom: 100px;
    }  
    
    #box2 {
      width: 100%;
      height: 200px;
      background-color: pink;
      margin-top: 50px;
    }

 

요로코롬 HTML, CSS가 작성돼있다고 하자. box1, box2는 형제 관계고 box는 밑 쪽으로 100px만큼의 margin을, box2는 위쪽으로 50px만큼의 margin을 가지고 싶어 한다. 상식적으론 둘의 의견을 반영해 150px의 margin이 생기는게 맞다고 생각되지만, 

 

 

실제론 이렇게 100만큼의 margin만 가지게 된다. 이게 형제간 margin 병합 현상. 충돌이 일어날 때 더 큰 margin값이 채택된다는 얘기다.

 

 

부모 자식간 margin병합 현상

자식만 margin을 두고 싶지만 부모도 함께 margin이 적용되는 현상을 말한다.

 

  <div id="div">  
    <article></article>
  </div>
    #div {
      width: 100%;
      height: 400px;
      background-color: yellow;
    }
    
    article {
      width: 100px;
      height: 100px;
      background-color: red;
    }

 

요로코롬 HTML, CSS가 작성돼있다고 하자. 현재 화면은 다음과 같다.

 

 

여기서 노란색은 놔두고 빨간색 박스만 밑으로 50px만큼 움직이고 싶어서 빨간색 박스(article태그)에 margin-top을 다음과 같이 먹였다고 하자.

 

    #div {
      width: 100%;
      height: 400px;
      background-color: yellow;
    }
    
    article {
      width: 100px;
      height: 100px;
      background-color: red;
      margin-top: 50px;
    }

 

하지만 결과는,

 

 

이렇게 부모도 같이 움직이게 된다. 이를 부모 자식간 margin병합 현상이라 한다. 자식에 쓰인 margin이 부모에게도 적용되는 것!

 

참고로 이건 자식에게 position값으로 absolute를 멕이는 것으로 해결 가능하다. absolute가 부모를 기준으로 적용되는 position값이기 때문!

+ Recent posts