안드로이드

안드로이드 - 회원가입 구현하기

베노 2022. 3. 26. 22:18

저번 포스팅에 이어서 회원가입을 진행하고 회원가입할때 유저들이 입력한 정보 토대로 데이터베이스에 저장시키는거까지 진행해 보자.

https://blog.veno.kr/12

 

안드로이드 - 로그인 구현하기

최근에 몸이 아프고 기침도 해서 블로그를 잠시 쉬었는데 게임하니깐 멀쩡해진거 같다. 그러니 블로그 다시 시작 https://blog.veno.kr/8 안드로이드 - 서버연동 안드로이드 앱 제작은 채팅앱 기준으

blog.veno.kr

 

파이어베이스에서 회원가입을 진행할 때 총 2가지의 메뉴를 사용한다.

 

첫 번째는 회원가입을 했을 때 사용자들을 관리하기 위한 Authentication이다.

여기서 회원가입을 한 사용자들의 리스트가 올라온다.

 

두 번째는 데이터베이스이다.

데이터베이스는 정보 저장하기 위해 사용한다.

데이터 베이스는 총 2가지이다.

Realtime DataBase와 Firestore Database이다.

 

Firestore : 모바일앱 개발을 위한 Firebase의 최신 데이터베이스로 실시간 데이터베이스의 성공을 바탕으로 더욱 직관적인 새로운 데이터 모델을 선보인다. 또한 실시간 데이터베이스보다 풍부하고 빠른 쿼리와 원활한 확장성을 제공

 

Realtime : Firebase의 기존 데이터베이스로, 여러 클라이언트에서 실시간으로 상태를 동기화해야하는 모바일앱을 위한 효율적이고 지연시간이 짧은 설루션

 

각 데이터베이스들의 장단점이 있으니 이거는 공식문서를 읽고 본인이 개발할 때 본인 입맛에 맞게 하면 될 거 같다.

https://firebase.google.com/docs/firestore/rtdb-vs-firestore?hl=ko 

 

데이터베이스 선택: Cloud Firestore 또는 실시간 데이터베이스  |  Firebase Documentation

Join Firebase at Google I/O online May 11-12, 2022. Register now 의견 보내기 데이터베이스 선택: Cloud Firestore 또는 실시간 데이터베이스 Firebase는 실시간 데이터 동기화를 지원하며 클라이언트에서 액세스할

firebase.google.com

 

본인은 FireStroe로 진행한다.

 

먼저 데이터베이스를 작성하기 전에 FireStore의 구조부터 파악해야 한다.

 

데이터를 저장할 때는 위와 같은 구조로 저장이 가능하다.

쉽게 컴퓨터의 파일과 폴더의 구조라고 보면 더 쉽다.

 

FireStore에서는 "컬렉션"과 "문서", "필드"가 존재한다.

위 사진과 같은 구조이다. "컬렉션"이라는 폴더의 "문서"라는 종이에 "data"라는 값들이 있으며, 필드가 "data"이다.

이제 구조를 알았으니 데이터를 저장시키거나 불러올 때 참고하면 된다.

 

 


액티비티 디자인하기


 

이 화면이 디자인 부분이다. 우리가 디자인을 한 그대로 안드로이드 앱 화면에 표시된다.

 

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SignUpActivity"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ProgressBar
            android:layout_gravity="center"
            android:indeterminateTint="@color/black"
            android:gravity="center"
            android:visibility="gone"
            android:id="@+id/signup_progress"
            android:layout_width="match_parent"
            android:layout_height="100dp"/>

    </LinearLayout>



    <ImageView
        android:id="@+id/signUp_image"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:src="@drawable/veno_size_black"/>

    <RelativeLayout
        android:layout_below="@+id/signUp_image"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:background="@color/background_color">

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/input_signup_email"
            android:layout_width="match_parent"
            android:backgroundTint="@color/black"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColorLink="@color/black"
            android:textColor="@color/black"
            android:inputType="textEmailAddress"
            android:layout_marginTop="20sp"
            android:layout_marginStart="20sp"
            android:layout_marginEnd="20sp"
            android:hint="@string/email"
            tools:ignore="NotSibling" />


        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/input_nickname"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/input_signup_email"
            android:layout_marginStart="20sp"
            android:layout_marginTop="10sp"
            android:layout_marginEnd="20sp"
            android:backgroundTint="@color/black"
            android:hint="@string/nickname"
            android:inputType="text"
            android:textColor="@color/black"
            android:textSize="16sp" />
        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/input_signup_password"
            android:backgroundTint="@color/black"
            android:textSize="16sp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10sp"
            android:layout_marginStart="20sp"
            android:textColor="@color/black"
            android:layout_below="@+id/input_nickname"
            android:inputType="textPassword"
            android:layout_marginEnd="20sp"
            android:hint="@string/password" />

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/input_confirm_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/input_signup_password"
            android:layout_marginStart="20sp"
            android:layout_marginTop="10sp"
            android:layout_marginEnd="20sp"
            android:hint="@string/password_confirm"
            android:backgroundTint="@color/black"
            android:inputType="textPassword"
            android:textColor="@color/black"
            android:textSize="16sp" />



        <LinearLayout
            android:layout_below="@+id/input_confirm_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/info_text"
            android:layout_marginTop="20sp"
            android:layout_marginStart="20sp"
            android:layout_marginEnd="20sp"
            android:gravity="center"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="15sp"
                android:text="@string/have_account"
                android:textColor="@color/black"/>
            <TextView
                android:id="@+id/logjn_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="15sp"
                android:textColor="@color/sky_blue"
                android:layout_marginStart="10sp"
                android:text="@string/login"/>
        </LinearLayout>

        <Button
            android:id="@+id/signup_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/info_text"
            android:layout_marginStart="20sp"
            android:layout_marginTop="32sp"
            android:layout_marginEnd="20sp"
            android:background="@drawable/button_disign1"
            android:text="@string/signUp"
            android:textColor="@color/black"
            android:textSize="20sp" />

    </RelativeLayout>

</RelativeLayout>

딱히 뭐 설명한 건 없지만 여기서 "password"와 "password_confirm"이 있다. 비밀번호와 비밀번호 확인하는 거다.

회원가입을 할 때 비밀번호가 맞는지 확인하기 위해 비밀번호 관련 인풋은 2개가 필요하다.

아마 안드로이드 처음 시도하는 사람들은 아래 코드는 생소할 거다.

android:hint="@string/password"

hint는 인풋 바에서 나오는 글자이다. 힌트로 지정해야 비밀번호를 입력할 때 해당 텍스트가 글자를 입력하자마자 사라지기 때문이고 힌트가 아닌 그냥 텍스트로 지정하면 "비밀번호"라는 글자를 지운 다음 비밀번호를 지정해줘야 하기 때문이다. 힌트로 지정하지 않고 텍스트로 한다면 아마 엉뚱한 값이 올라갈 것이다.

@string/password는 string파일에 있는 password라는 이름을 가진 녀석을 가져온다는 것이다.

왜 text가 아닌 string파일에 있는 정보를 가져올까 

 

이유는 단순하다. 어디서든 쉽게 꺼내고 일일이 비밀번호라고 안쳐도 된다. 또한 언어 설정할 때도 쉽게 바꿔주기 위함이다.

 

 


액티비티 조작하기


 

class SignUpActivity : AppCompatActivity() {

    var auth: FirebaseAuth? = null
    var database: FirebaseFirestore? = null

    override fun onCreate(savedInstanceState: Bundle?) {

        auth = FirebaseAuth.getInstance()
        database = FirebaseFirestore.getInstance()

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sign_up)

        signup_btn.setOnClickListener { checking_input()}
    }

    fun checking_input() {
        if (input_signup_email.text.toString().isEmpty() || input_nickname.text.toString()
                .isEmpty() || input_signup_password.text.toString()
                .isEmpty() || input_confirm_password.text.toString().isEmpty()
        ) {
            Toast.makeText(this, R.string.empty_input_Signup_text, Toast.LENGTH_SHORT).show()
        } else if (input_signup_password.text.toString() != input_confirm_password.text.toString()) {
            Toast.makeText(this, R.string.not_match_password, Toast.LENGTH_SHORT).show()
        } else {
            createUser()
        }
    }

    @SuppressLint("ShowToast")
    fun createUser() {
        signup_progress.visibility = View.VISIBLE
        auth?.createUserWithEmailAndPassword(input_signup_email.text.toString(), input_signup_password.text.toString())?.addOnCompleteListener { task ->
            if (task.isSuccessful) {
            val user_email = task.result.user?.email
            var userModel = UserModel()

            userModel.userEmail = input_signup_email.text.toString()
            userModel.userName = input_nickname.text.toString()
            userModel.uid = FirebaseAuth.getInstance().uid

            signup_progress.visibility = View.GONE

            if (user_email != null) {
                database?.collection("user")?.document(user_email)?.set(userModel)
                    ?.addOnCompleteListener { task ->
                        if (task.isSuccessful) {
//                            인텐트 자리
                            finish()
                        }
                    }
                }
            }
        }
    }
}

이제부터 내가 노가다를 조지게 하면 된다.

이걸 보는 사람들은 그냥 읽고 억지로 이해시키면 된다.

이해 안 된다면 무조건 해라 그게 안드로이드이다.

불만 있으면 구글 본사에 미사일 폭격을 날려라

 

var auth: FirebaseAuth? = null
var database: FirebaseFirestore? = null

auth라는 것과 database라는 걸 초기화시켜준다.

 

 

auth = FirebaseAuth.getInstance()
database = FirebaseFirestore.getInstance()

다시 auth와 database를 인스턴스를 지정해준다.

 

signup_btn.setOnClickListener { checking_input()}

signup_btn을 클릭했을 때 checking_input으로 가서 로그인 함수와 똑같이 인풋에 빈 값이 있는지 체크해주면 된다.

 

 

fun checking_input() {
    if (input_signup_email.text.toString().isEmpty() || input_nickname.text.toString().isEmpty() || input_signup_password.text.toString().isEmpty() || input_confirm_password.text.toString().isEmpty()
    ) {
        Toast.makeText(this, R.string.empty_input_Signup_text, Toast.LENGTH_SHORT).show()
    } else if (input_signup_password.text.toString() != input_confirm_password.text.toString()) {
        Toast.makeText(this, R.string.not_match_password, Toast.LENGTH_SHORT).show()
    } else {
        createUser()
    }
}

if문으로 조건을 건다.

만약 input_signup_email과 input_nickname과 input_signup_password과 input_confirm_password이 빈값

즉 모든 인풋에 텍스트가 없다면 if문 안쪽 코드를 실행한다.

Toast.makeText(this, R.string.empty_input_Signup_text, Toast.LENGTH_SHORT).show()

토스트로 빈 값이니 텍스트를 채워달라고 메시지를 띄우면 된다.

 

else로 조건이 충족하지 않을 때 코드를 실행한다.

 

else if (input_signup_password.text.toString() != input_confirm_password.text.toString()) {
    Toast.makeText(this, R.string.not_match_password, Toast.LENGTH_SHORT).show()
}

하지만 새로운 형태의 녀석이 나왔다.

else지만 else앞에 if가 붙어있다

영단어 그대로 해석하면 된다.

else if, 만약 ~~가 아니라 ~~이라면

조건을 여러 번 할 때 쓰는 거다. 

첫 번째 if문의 조건이 아닌 다른 조건일 때이다.

input_signup_password과 input_confirm_password는 비밀번호와 비밀번호 확인 인풋이다.

당연히 확인하는 것이기 위해 두 개의 값이 동일해야 한다.

연산자로 같다(==)의 반대 같지 않다 (!=)를 달아준다.

input_signup_password과 input_confirm_password의 텍스트 값이 같지 않다면 아래 함수를 실행해준다.

 

Toast.makeText(this, R.string.not_match_password, Toast.LENGTH_SHORT).show()

같지 않으니 다시 확인하라는 토스트 메시지를 띄우면 된다.

 

else {
    createUser()
}

두 개의 조건문 중 에서 다 조건에 충족하지 않으면 createUser라는 함수를 실행해준다.

 

fun createUser() {
        signup_progress.visibility = View.VISIBLE
        auth?.createUserWithEmailAndPassword(input_signup_email.text.toString(), input_signup_password.text.toString())?.addOnCompleteListener { task ->
            if (task.isSuccessful) {
            val user_email = task.result.user?.email
            var userModel = UserModel()

            userModel.userEmail = input_signup_email.text.toString()
            userModel.userName = input_nickname.text.toString()
            userModel.uid = FirebaseAuth.getInstance().uid

            signup_progress.visibility = View.GONE

            if (user_email != null) {
                database?.collection("user")?.document(user_email)?.set(userModel)
                    ?.addOnCompleteListener { task ->
                        if (task.isSuccessful) {
                            finish()
                        }
                    }
                }
            }
        }
    }
signup_progress.visibility = View.VISIBLE

프로그래스이다. 저번에 포스팅한 로그인에서 설명 실컷했으니 참고해라

 

auth?.createUserWithEmailAndPassword(input_signup_email.text.toString(), input_signup_password.text.toString())?.addOnCompleteListener { task ->

 

이 코드는 회원가입해주는 코드이다. 

createUserWithEmailAndPassword라는 파이어베이스에서 제공해주는 리스너를 사용한다.

이 리스너는 회원가입 리스너이다. 서버에서 "저 회원가입좀 시도하겠습니다!" 라고 신호를 보내는거다.

하지만 리스너의 이름에서 알수있다시피 create만들다 User사용자 With~와함께 Email이메일 And그리고 Password비밀번호

 

회원가입을 할때 계정을 만들어야 한다.

유저를 만들때 이메일과 비밀번호를 함께 사용해서 계정을 만든다 라는뜻이다.

그럼 당연히 이메일과 비밀번호가 필요하다

그래서 input_signup_email과 input_signup_password의 텍스트값을 가져온다

 

"하지만 비밀번호 관련 인풋2개인데 왜 하나만 쓰지?"라는 의문점을 가질수도 있다.

우리가 위에서 이미 두개의 비밀번호 관련 인풋을 동일한 값인지를 조건으로 걸어 검사를 했다.

input_signup_password과 input_confirm_password중에서 아무거나 사용해도된다.

두개중 한개만 써도 동일한 값이기 때문이다.

 

그럼 일단 회원가입을 위해 계정을 생성해야 하는데 게정을 생성하기위해서는 이메일값이랑 비밀번호값을 전달했다.

그럼이제 이게 처리를 정상적으로 완료했다는 신호를 주고 그에 맞는 코드를 입력해 추가적으로 작업을 진행하면 된다.

 

addOnCompleteListener를 사용하여 리스너에 대한 처리를 진행 해 주면 된다. 그 신호를 task로 지정해준다.

굳이 task가 아니여도 된다. 원하는걸로 해 주면 된다. 예를 들면 task가 아닌 venoIdiot으로 해도 된다.

if (task.isSuccessful) {

}

나는 task로 해줬기 때문에 task에 대한 성공적으로 했는지 안했는지 구별한다.

그것도 if문으로 조건을 충족시켜준다.

만약 task가 isSuccessful(성공적)이라면 if문 안에 있는 코드를 진행 해 준다.

 

val user_email = task.result.user?.email
var userModel = UserModel()

userModel.userEmail = input_signup_email.text.toString()
userModel.userName = input_nickname.text.toString()
userModel.uid = FirebaseAuth.getInstance().uid

여기서 user_email이라는 변수를 만들어준다 이 변수에는 task.result.user.email이 들어가있다.

task.result.user.email이 값은 유저의 이메일값을 가져오는거다.

이미 회원가입이 완료되어있기때문에 이메일은 우리가 입력했던 이메일 값이 들어있다.

 

그래서 이메일값을 가져와서 user_email이라는 변수에 저장시켜주는거다.

 

그 다음은 userModel이라는 변수를 만들어주고 이 변수에 UserModel()이라는 파일을 가져온다.

이 파일은 서버에 저장하기 위해 여러정보를 받은값을 UserMdoel()이라는 파일 토대로 업로드가 된다.

 

그전에 당연히 UserMdoel이라는 파일을 만들어줘야 한다.

여기서는 파일이라고 명칭했지만 다음부터는 클래스파일이라고 설명한다.

 

당연히 코틀린언어로 작성중이니 코틀린 클래스 및 파일으로 클래스파일을 생성해준다.

 

Data Class라는 타입으로 UserMdoel이름으로 클래스파일을 생성해준다.

 

 

 

UserModel.kt

data class UserModel(
    var userName : String? = null,
    var userImage : String? = null,
    var userEmail : String? = null,
    var uid : String? = null,
    var admin: String? = null,
    var admin_value: Boolean = false,
    var point: Long? = 0
)

여러가지의 변수들이 많다. 이 변수들 기반으로 데이터베이스에 보내줄것이다.

본인들이 이 데이터클래스파일에 있는 변수를 수정하면 된다.

변수를 지우거나 추가로 더 변수를 만들면 된다.

 

userName은 사용자 닉네임

userImage는 사용자 프로필 이미지

userEmail은 사용자 이메일

uid는 사용자 id

admin은 관리자

admin_value는 관리자인지 아닌지 검사

poin는 앱 내에서 사용하게 될 포인트이다.

 

각 변수들에 맞게 타입을 지정하면 된다. 문자열은 String, 숫자는 Long, 참/거짓만 있게 할려면 Boolean을 사용하면 되지만 Boolean은 null값이 없다. 그래서 true, false중 하나만 주면 된다. 기본적으로 poin를 제외하고 다 빈값(null)으로 줬다. 어차피 나중에 추가할것이기 때문이다.

 

var userModel = UserModel()

이 코드는 UserModel이라는 클래스파일을 userModel이라는 변수에 지정해준다.

그냥 쉽게 쓰기 위해 적은거다. 사실 글자는 별 차이는 없다.

 

userModel.userEmail = input_signup_email.text.toString()
userModel.userName = input_nickname.text.toString()
userModel.uid = FirebaseAuth.getInstance().uid

userMdoel.userEmail은 이메일이기 때문에 이메일 값이 와야 한다. 

input_signup_email이라는 인풋에서 가져오면 된다.

 

userMdoel.userName은 닉네임이기 때문에 닉네임 값이 와야 한다.

input_nickname이라는 인풋에서 가져오면 된다.

 

userMdoel.uid는 해당 유저를 관리하기 위해 사용자 id를 가져와야 한다.

uid는 user id로 회원가입을 진행하게 되면 authentication에서 자동으로 만들어준다. 이거는 일종의 주민등록번호라고 보면 된다. 중첩이 안되고 삭제하기전까지 영구적으로 가져가는 번호이다.

만약 회원가입이 성공적으로 이뤄지면 authentication에서 이렇게 리스트가 붙는다. 이때 여기서 사용자 uid가 뜬다.

이 값은 FirebaseAuth에서 관리해주고 값을 준다.

필요할때 FirebaseAuth를 불러오면 된다.

불러올땐 FirebaseAuth.getInstance.uid로 uid를 요청해서 받아오고 그걸 userMdoel의 클래스파일에 uid 변수에 저장시켜준다.

 

그러면 UserMdoel에 있는 변수들에 값을 저장시켜줬다. 

 

이제 이 UserMdoel의 클래스파일을 가지고 서버에 전송해서 데이터베이스에 저장시키면 된다.

 

if (user_email != null) {
    database?.collection("user")?.document(user_email)?.set(userModel)
        ?.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                finish()
            }
        }
    }
}

일단 if문으로 user_email이 빈값이 아닌지 체크한다. 이거는 이메일값이 있는지 없는지를 체크해서 본인 게정이 맞을때 전송시켜주는거다.

 

database?.collection("user")?.document(user_email)?.set(userModel)?.addOnCompleteListener

위에서 선언한 데이터베이스의 변수 database를 가져오고 collection으로 "user"라는 컬렉션을 만들어준다.

그 다음 문서이름은 사용자를 쉽게 찾기위해 이메일로 저장시켜준다. 이메일은 변수 user_email에 저장된 정보를 가져와 준다. set은 데이터베이스에 값을 저장시켜주는거다. 값은 당연히 UserMdoel에 있는 변수들을 전부 가져가서 붙여준다. 이제 이 리스너가 처리되었는지를 확인하기 위해 addOnCompleteListener를 적어준다. 나는 신호를 task로 지정.

if (task.isSuccessful) {
    finish()
}

만약 task가 isSuccessful(성공적)이라면 if문 안에 있는 코드를 진행 해 준다.

finish로 회원가입화면을 끝장내주면 된다.

 

이렇게 하면 데이터베이스에서는 아래와 같은 정보를 저장시켜준다.

 

collection("user")로 했기 때문에 컬렉션이름은 user로 되었고, document(user_email)로 했기 때문에 문서이름은 이메일값이 들어가졌다.

set(userModel)로 했기 때문에 문서의 값들은 userModel(UserMdoel)에 있는 변수들이 전부 들어가졌다.

몇몇개는 null이 보인다.

이렇게 기본적으로 null줬기 때문에 우리가 변수에 추가한 값들 외에는 null이 들어가있다.

"어? 그럼 사용자 프로필은 왜 없냐!"라고 할텐데 이거는 내가 귀찮아서 아직 구현을 안했다.

 

나중에 사용자 정보 수정할때 프로필 이미지에대한것도 다룰거니 그때까지 살아있으면 된다 ㅇㅇ

 

이렇게 하면 회원가입과 로그인은 모두 끝났다.

하지만 문제가 있다.

 

내가 저번에 올렸던 포스팅을 보면 

로그인 할때 "진짜 존재하는 이메일인지 판단해야한다"라고해서 이메일로 인증코드를 보내 그게 활성화가 되었는지 안되었는지 판단하는 코드를 올렸다. 

 

문제는 파이어베이스 공식문서에서 "회원가입을 하게 되면 로그인도 자동으로 된다."라고 했다.

그래서 회원가입이 완료되면 로그인 화면으로 가지 않고 로그인 화면에 있던

override fun onStart() {
    super.onStart()
    movepage(auth?.currentUser)
}

이 함수가 실행되고 자동으로 로그인이 된다.

여기에 있는 movepage 함수 

fun movepage(user: FirebaseUser?){
    if(user != null){
        startActivity(Intent(this, MainActivity::class.java))
        finish()
    }
}

이 함수에서 user가 빈값이 아니라고 판단하고 바로 로그인까지 된다.

 

내가 원하는건 "회원가입 성공하게 되면 로그인 화면으로 가서 로그인할때 그 유저가 이메일 인증된 사람인지 아닌지"를 판단해야 하는데 공식문서대로 로그인화면으로 가지않고 회원가입과 동시에 로그인이 되어버린다.

그래서 movepage가 인식해서 바로 MainActivity로 넘겨준다.

 

그래서 어쩔수 없이 MainActivity에서 다시 로그인 화면으로 보내줘야 한다.

그럼 어떻게 해야하나?

 

MainActivity에서 이메일 인증된 사람인지를 체크만 해 준다.

이메일이 인증된 사람이라면 그냥 사용가능하도록 냅두고 인증 안된 사용자라면 로그아웃처리 해 주고 로그인 화면으로 이동 시켜준다.

로그아웃은 회원가입과 동시에 로그인이 되버렸기 때문에 로그아웃을 해 줘야 한다.

 

그럼 내가 원하는대로 작동이 된다.

 

약간 뭐랄까 내가 잘생겨지고 싶어서 성형수술 했는데 코가 잘못되어서 보형물을 억지로 넣은것처럼 된거라고 보면 될거같다.

 

이 부분은 다음에 포스팅 할 안드로이드 부분에서 다시 다룰거다. 그러니 큰 걱정 하지 말고 걍 본인 할 일 하면된다.

 

 


끝으로


진짜 안드로이드 개발하면서 엿같은 경험을 많이 했다.

물론 모바일시장은 언제나 활발해서 살아남기 쉬울거같지만 앱 개발하는 입장에서는 아이폰도 해야하는 생각이 들것이다.

차라리 안드로이드를 버리고 아이폰 앱 개발을 해라 구글은 걍 더러운 녀석들이다.

코틀린 언어 자체는 진짜 섹시한 언어인데 더러운 구글을 만나서 안드로이드에 쑤셔박으니 진짜 엿같아지는거같다.

이래서 안드로이드에서 아직 못빠져나온거같다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

이러니 내가 게임에 현질하는 흑우가 되는거지 ㅆㅂ