Blog

  • 맥북 스크린 캡쳐 기본 폴더 변경하기

    맥북에서 스크린 캡쳐를 하다보면 지저분하게 바탕화면에 저장된다.

    기본 캡쳐 폴더 변경 방법

    Shift + Command + 5 를 누르자.

    Option -> Other Location을 선택하자.

    위와 같이 하면 다음부터는 스크린 캡쳐시 위에서 지정한 폴더에 저장될 것이다.

  • 페이지뷰 = 블로그 수익 블로그로 돈 버는 방법

    페이지뷰 = 블로그 수익 블로그로 돈 버는 방법

    블로그를 운영하는 입장에서 과연 내가 언제 돈을 벌 수 있을까 다들 한번 쯤은 고민을 한다.

    이와 관련된 좋은 글이 있어서 요약해봤다.

    원문

    블로그 수익

    구글 애드센스

    가장 널리 알려진 방법이다. 누군가 광고를 보고 클릭하면 돈을 벌 수 있다.

    제휴 광고 (Affiliate Marketing)

    개인적으로 제휴 광고는 블로그 주제와 맞는 것을 선택하는 것이 맞다고 생각한다. 예를 들어 개발자면 Udemy 강의에 대한 제휴 광고 링크를 블로그 글에 추가하는 방식이다. 내가 제품을 리뷰하는 블로거라면 아마존 제휴 광고 링크를 넣는것이 어울릴 것이다. 그리고 해당 링크를 사람들이 클릭하면 그에 대한 트래픽에 따라 돈을 주는 방식이다.

    아마존의 제휴 마케팅 광고 (https://affiliate-program.amazon.com)

    유데미(Udemy)의 제휴 마케팅 광고 (https://www.udemy.com/affiliate)

    Trip.com의 제휴 마케팅 (https://www.trip.com/partners/index)

    스폰서 포스팅 (협찬 받아서 글 써주고 소정의 금전적인 지급 받기)

    네이버 블로그에서 흔히 많이 보이는 “해당 포스트는 xx 로부터 협찬을 받아 작성된 글입니다” 글들이 이에 해당된다.

    이메일 구독

    워드프레스에도 있는 기능인데 블로그 방문하는 사람들이 내 블로그를 구독할 수 있도록 기능을 제공하는 것이다. 방문자가 내 블로그를 구독하게 되면 내가 글을 올릴 때마다 이메일로 알람이 간다. 이것은 구독자들이 지속적으로 나의 블로그에 방문하게 만들어준다. 즉 팬을 확보하는 작업이라 할 수 있다.

    개인적으로 블로그 글에 집중해서 차근차근 페이지뷰를 늘리고 난 다음에 구글 애드센스 + 제휴 광고 링크를 넣어서 수익화 하는 것이 장기적으로 봤을 때 가장 좋다고 생각한다.

    페이지 뷰 = 수익

    지금부터 말하는 단위는 한 달 기준이다. 페이지 뷰가 늘어날 수록 애드센스 수익 외에도 다른 수익 채널을 통해 더 많은 수익을 낼 수 있다. 아래는 단순히 애드센스 수익만 고려했다는 점 참고.

    월 페이지 뷰 1,000 = 구글 애드센스 예상 수익 5달러 미만

    유명인이 아닌 이상 블로그를 오픈하면 누구나 페이지 뷰 0으로 시작한다. 글을 꾸준히 올리면 한달에 1,000 페이지뷰에 달성하는 순간이 온다.

    월 페이지 뷰 1,000 – 10,000 = 구글 애드센스 + 제휴링크로 수익화 고려해야 될 단계

    전체 블로거의 40%가 이 구간에 있다고 함.

    이 구간에서 페이지뷰가 5,000이 넘을 경우 제휴링크를 추가해서 얻는 수익이 구글 애드센스 수익보다 많아지는 구간이다.

    구글 애드센스 수익은 1,000 페이지 뷰 = 2 – 12 달러 (블로그 주제에 따라 달라질 수 있다)

    예를 들어 페이지 뷰가 10,000이라면 애드센스 수익 약 20달러 – 120달러를 예상할 수 있다.

    월 페이지뷰 10,000 – 100,000

    페이지뷰가 60,000이 넘는다면 https://www.mediavine.com 와 같은 업체를 통해 수익화 구조를 확장하는 것이 도움이 되는 구간이다.

    애드센스 수익은 1,000 페이지 뷰당 최소 10달러이다.

    페이지뷰가 100,000일 경우 애드센스 수익으로만 약 1,000달러 예상할 수 있다.

    월 페이지 뷰 100,000 이상인 경우

    이 구간에 진입하게 된다면 스폰서 포스팅에 대한 가치가 상당히 높아져서 수익이 아주 좋다. (업체로부터 협찬을 받아서 글 쓰는 것)

    페이지뷰를 늘리는 전략 (틈새 시장 공략)

    • 남들이 블로그에 잘 안쓰는 주제로 포스팅 올리기
    • 글을 적당히 길게 쓰기 (최소 1800 글자 이상)
    • 글을 잘 쓰기 (대충 휘갈겨 쓰지않기, 성의것 도움이 되는 정보를 포스팅하자)

    위의 3가지 진짜 공감된다. 예를 들어 남들이 다 쓰는 주제는 내 블로그가 검색될 확률이 정말 낮다. 그 만큼 해당 검색결과로 나오는 블로그가 정말 많기 때문이다. 더군다나 이제 막 개설한 블로그라면 더욱 그렇다. (독서를 좋아한다면 책에 관련된 포스팅을 꾸준히 올리는 것도 분명 큰 도움이 될 것이다. 당장은 노출 안되도 언젠가는 터진다 만약 누군가가 해당 도서에 관심이 있다면..)

    누구나 자신만이 알고 있는 지식과 경험이 있다. 이런 글들을 꾸준히 올리는 것이 페이지뷰를 늘리는 데 정말 큰 도움이 된다.

    내 블로그 글 중에 백악관 투어 예약하기라는 글이 있다.

    많은 사람들이 워싱턴 DC를 방문하지만 백악관 내부를 둘러보지는 않는다. 하지만 누구나 백악관 내부를 구경해보고 싶다는 생각을 할 수 있다. 내가 이 글을 작성할 때만 해도 정말 검색해도 나오지 않는 정보였다. 그래서 그때 당시 나는 이미 경험했고 어떻게 예약해서 갈 수 있는지 알고있기 때문에 작성했다. 그래서 결과는?

    구글 검색결과 첫 페이지 중에서도 상단 4번째에 노출

    20만개의 검색 결과중에 탑 4다.

    검색결과를 보면 마이리얼트립이 가장 상단에 있다. 그리고 그 위에 스폰서라고 써있다. 즉 구글에 엄청난 비용을 지불해서 첫 페이지 최상단에 노출되는 결과를 얻었다. 그리고 두번째는 워싱턴 공식 페이지이고 세번째는 여행업체이다. 그 다음 네 번째가 바로 내 블로그이다.

    블로그 주소도 무료버전 티가 난다. 나만의 닷컴 주소도 아닌 wordpress.com 주소이다. 저때 당시 SEO에 대해서도 잘 몰랐다. 그저 검색해도 찾기 어려웠던 글을 올렸을 뿐인데 SEO에서 가장 중요한 구글의 첫 페이지 상단에 내 블로그 글을 노출 시켰다.

    마치며

    블로그로 돈 버는 방법을 썼지만 아이러니하게도 나는 아직 블로그로 돈을 번적이 없다. 내 블로그는 현재 월 페이지 뷰가 이제 1,000이 넘기 시작했다. 앞으로 싱가포르에서 소프트웨어 개발자로 일하면서 경험하는 소소한 내용들을 정리해서 포스팅을 꾸준히 할 예정이다. 많은 구독과 관심 주시면 더욱 더 열심히 포스팅 할 예정이다.

  • 싱가포르 탄종파가 녹차 전문점 hvala 강추

    싱가포르 탄종파가 녹차 전문점 hvala 강추

    오랜만에 싱가포르 현지인 커플을 만났다.

    점심을 먹고 디저트로 여기를 가보자고 해서 방문. 강추 강추 존맛탱

    외관

    hvala가 카페 이름인데 글씨가 엄청 작게 써있어서 잘 봐야된다.

    사이트도 있다.

    https://www.hvala.com.sg

    실내

    주문은 키오스크로 한다

    전형적인 싱가포르 가게구조. 길게 쭈욱 뻗어있는 가게들이 많다.

    메뉴

    아이스크림도 맛있고 녹차도 엄청 맛있게 먹었다. 녹차를 주문할 때 등급이 있는데 우리는 중간 등급으로 먹었다. 아주 만족스러운 녹차라떼였다.

    위치

    우리는 Craig Road 지점으로 갔었다. 싱가포르내에 여러지점이 있으니 아래 사이트 들어가서 가까운 곳 방문해보는 것을 추천함

    https://www.hvala.com.sg/pages/hvala-locations

  • 광주광역시 전남대병원 근처 커피 맛있는 곳

    전남대 병원 근처에 있는 카페가 있어서 찾아갔다. 결론부터 말하자면 커피를 정말 잘 하는 집이다. 나는 시그니쳐 커피를 주문했는데 굳이 비교하자면 강원도에 있는 툇마루 커피와 비슷한 메뉴이다.

    툇마루 커피도 정말 맛있었지만 여기 카페도 전혀 뒤쳐지지 않았다. 아니 오히려 좀 더 맛있게 느껴졌다.

    전남대 병원 근처에 올일이 있다면 꼭 가볼만한 카페다.

    위치

    요 건물 건너편 쯔음 위치한 카페다

    빌라가 많은 동네의 큰 길가에 있다. 가게 앞에 주차를 할 수 있지만 1대 정도만 주차할 수 있어 보였다.

    카페 내부

    카페 사장님이 정성스럽게 커피를 내려주고 있었다
    디저트 메뉴도 다 맛있어 보였다.

    시그니처 커피

    드디어 내가 주문한 커피가 나왔다. 실내 공간이 협소해서 앉을 자리가 없었다. 아쉽지만 테이크 아웃을 했다.

    에스프레소와 크림의 조합이 아주 기가 막혔다. 조금 달달한 메뉴이긴 한데 무작정 달기만 하지도 않고 조합이 정말 좋았다. 너무 맛있게 마지막 한 모금까지 마셨다.

    마치며

    커피맛으로만 따지자면 아주아주 훌륭했다. 다만 조금 아쉬웠던 점은 주차공간이 없다는 점과 몇 없는 실내 테이블 좌석을 몇몇 손님이 거의 독차지 할 수 밖에 없는 구조다.

    예를 들어 4명이 앉을 수 있는 테이블에 손님 두명이 앉아있고 그 옆에 가방을 의자위에 올려놓고 차지하고 있었다.

    이런 손님들에게 사장님이 옆에 다른 사람이 앉을 수 있도록 안내를 좀 해줬으면 좋았을 텐데 그 부분이 아쉬웠다.

  • 싱가포르 힐튼 싱가포르 오차드(예전 이름, 만다린 오차드 싱가포르) 후기

    싱가포르 힐튼 싱가포르 오차드(예전 이름, 만다린 오차드 싱가포르) 후기

    2020년 결혼기념일 여행으로 싱가포르에 갔었다

    그때 당시 마리나베이샌즈에서도 1박 했었는데 그 기억이 강렬해서인지 이 호텔에 대한 기억이 희미해졌다

    사진첩과 여행 계획표를 에버노트에서 찾다가 우리가 묶었던 호텔이 만다린 오차드였다는 것을 확인

    관련기사

    참고로 이 호텔은 현재 힐튼으로 리브랜딩 중인걸로 확인했고 리노베이션 공사중인지 암튼 지금은 잠시 문 닫은 상태다

    객실 사진

    결혼기념일이라고 했더니 객실에 신경써줬다
    결혼기념일 케익도 준비해줬었다

    객실 뷰

    시티뷰다 근처에 여러 호텔들이 보인다

    조식

    조식 맛있었던 기억이 난다. 약간 중국식? 메뉴들이 많았었다. 난 콘지를 좋아해서 이거저거 토핑해서 먹었었다.

    마치며

    위치도 진짜 좋고 전철이랑도 가까운 편이며 객실 청결 상태도 좋았다.

  • 싱가포르 탄종파가에서 줄서서 먹는 빵집 챔피언

    싱가포르 탄종파가에서 줄서서 먹는 빵집 챔피언

    탄종파가를 다녀왔다

    줄서서 먹는 빵집으로 유명한 챔피언이다. 여기는 홍콩에서 들여온 브랜드라고 한다

    특히나 유명한 빵은 카레빵인데 이날 품절되서 다른 인기있는 소브르 빵을 사먹었다

    챔피언 빵집

    품절된 메뉴들
    테이크아웃해서 집에 가져옴
    소부르 빵 + 버터 + 치즈
    존맛이다
    다먹고 보니 빵을 20분안에 못먹었다면 영수증 지참해서 가져와도 된다는 메세지 발견. 새로운 빵으로 교환해준다

    총평

    빵을 좋아한다면 강추. 사람들이 줄서는 것은 다 이유가 있다

  • How to write a unit test for validating regex without using XCUIElement’s typeText function

    How to write a unit test for validating regex without using XCUIElement’s typeText function

    This post might be helpful if you are considering using the typeText function for validating user input using regular expressions, such as for validating passwords or usernames.

    Although XCTest enables you to write unit tests for validating the results of regular expressions, it is not suitable for writing test cases that involve inputting characters one by one.

    Let me show the example.

    Business requirements

    • Username allows alphabet + numbers only

    • Password allows alphabet + numbers + special characters

    • Address allows alphabet + numbers only

    • Phone number format is 4 digits – 4 digits

    • Postal Code is 5 numbers

    • Birthday format is yyyyMMDD

    • Username, Password, Phone, Address are required fields

    • Error message will be displayed when entering text into the TextField

    • Search button will be enabled once the postal code is validated.

    Let’s write a test cases

    ProfileInputValidatorTests: XCTestCase

    It has a sut for checking unitTests and typeText function (like a XCUIElement’s typeText) to put the characters into the active UITextField.

    class ProfileInputValidatorTests: XCTestCase {
        var sut: MockClass!
    
        override func setUp() {
            super.setUp()
            sut = MockClass()
        }
    
        override func tearDown() {
            super.tearDown()
            sut.activeTextField = nil
            sut.inputCheckType = nil
            sut.errorMessage = nil
        }
    
        //UnitTests
    }
    
    private extension ProfileInputValidatorTests {
        func typeText(_ input: String) {
            guard let activeTextField = sut.activeTextField else {
                return
            }
            for (location, character) in input.enumerated() {
                let canInputText = activeTextField.delegate?.textField?(
                    activeTextField,
                    shouldChangeCharactersIn: NSRange(location: location, length: 0),
                    replacementString: String(character)
                )
                if canInputText ?? false {
                    let lastPosition = activeTextField.endOfDocument
                    activeTextField.selectedTextRange = activeTextField
                        .textRange(from: lastPosition, to: lastPosition)
                    activeTextField.insertText(String(character))
                }
            }
        }
    }

    Here is the example of unit tests

    func test_textField_should_not_exceed_maxCharacter() {
        ProfileInputType.allCases.forEach {
            sut.setActiveTextField($0)
            let dummyText = """
            Lorem Ipsum is simply dummy text of the printing and typesetting industry.
            """
            typeText(dummyText)
            XCTAssertTrue(
                sut.activeTextField?.text?.count == sut.inputValidator.activeTextFieldType?.maxCharacter,
                "TextField tag: \($0.rawValue) should not exceed maxCharacter"
            )
        }
    }
    
    func test_username_allowance_character() {
        let username = "skboard"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_username_not_allowance_character() {
        let username = "マーン224s한글djfタklsdf"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertEqual(ProfileInputType.username.errorMessage, sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_postal_code_allowance_character() {
        let postalCode = "13456"
        sut.setActiveTextField(.postalCode)
        typeText(postalCode)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.postalCode)
    }
    
    func test_is_enabled_search_button_while_editing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "89761"
        typeText(postalCode)
        XCTAssertEqual(sut.activeTextField?.text, "89761")
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_is_enabled_search_button_after_didEndEditing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "24028"
        typeText(postalCode)
    
        XCTAssertEqual(sut.activeTextField?.text, "24028")
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_phone_number_format() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "78761241"
        typeText(phoneNumber)
        XCTAssertEqual(sut.activeTextField?.text, "7876-1241")
    }
    
    func test_birthday_format() {
        let birthday = "19980115"
        sut.setActiveTextField(.birthday)
        typeText(birthday)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.birthday)
    }
    
    func test_phone_number_only_allow_one_hyphen_character() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "1467834527891471247"
        typeText(phoneNumber)
        let hyphenCount = numberOfHyphen(in: sut.activeTextField?.text ?? "")
        XCTAssertTrue(
            hyphenCount == 1,
            "phone number only allow one hyphen character"
        )
    }

    InputValidator

    InputValidator checks the inputTypes and validates strings.

    protocol ProfileInputValidatorDelegate: AnyObject {
        var activeTextField: UITextField? { get set }
        func setActiveTextField(_ inputType: ProfileInputType)
        func updateAddressSearchButtonState(isEnabled: Bool)
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?)
    }
    
    final class ProfileInputValidator: NSObject {
        var activeTextFieldType: ProfileInputType?
        var dateFormatter: DateFormatter?
        weak var delegate: ProfileInputValidatorDelegate?
        
        func validate(
            text: String,
            type: ProfileInputType,
            needToSetType: Bool = false,
            placeholder: String? = nil
        ) {
            guard isNotEmpty(text: text) else {
                showEmptyMessage(type: type, needToSetType: needToSetType, placeholder: placeholder)
                return
            }
            let errorMessage: String?
            switch type {
            case .username:
                errorMessage = isValidUsername(text) ? nil : type.errorMessage
            case .password:
                errorMessage = isValidPassword(text) ? nil : type.errorMessage
            case .phone:
                errorMessage = isValidPhoneNumber(text) ? nil : type.errorMessage
            case .address:
                errorMessage = isValidAddress(text) ? nil : type.errorMessage
            case .postalCode:
                errorMessage = isValidPostalCode(text) ? nil : type.errorMessage
            case .birthday:
                errorMessage = isValidBirthday(text) ? nil : type.errorMessage
            }
            delegate?.showValidateResult(needToSetType ? type : activeTextFieldType, error: errorMessage)
        }
        
        private var isRequiredField: Bool {
            if activeTextFieldType == .some(.username) ||
                activeTextFieldType == .some(.password) ||
                activeTextFieldType == .some(.address) ||
                activeTextFieldType == .some(.phone) {
                return true
            }
            else {
                return false
            }
        }
        
        private func showEmptyMessage(type: ProfileInputType, needToSetType: Bool, placeholder: String?) {
            let textFieldTitle = activeTextFieldType?.title ?? ""
            var emptyErrorMessage: String?
            if isRequiredField {
                emptyErrorMessage =
                    "\(placeholder ?? textFieldTitle) is required"
            }
            delegate?.showValidateResult(
                needToSetType ? type : activeTextFieldType,
                error: emptyErrorMessage
            )
        }
    
        private func isNotEmpty(text: String?) -> Bool {
            guard let inputText = text else {
                return false
            }
            let textWithOutWhiteSpace = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
            return !textWithOutWhiteSpace.isEmpty
        }
    
        private func isValidDateFormat(text: String) -> Bool {
            guard convertBirthdayFormat(text)?.count == ProfileInputType.birthday.maxCharacter else {
                return false
            }
            return true
        }
    }

    This class conforms to UITextFieldDelegate to check strings

    //MARK: UITextFieldDelegate
    extension ProfileInputValidator: UITextFieldDelegate {
        func textFieldDidBeginEditing(_ textField: UITextField) {
            guard let fieldType = ProfileInputType(textField.tag) else {
                return
            }
            delegate?.activeTextField = textField
            activeTextFieldType = fieldType
        }
    
        func textFieldDidEndEditing(_ textField: UITextField) {
            guard let inputText = textField.text, let inputType = activeTextFieldType else {
                return
            }
            validate(text: inputText, type: inputType)
        }
    
        func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool {
            let text = textField.text ?? ""
            guard let stringRange = Range(range, in: text),
                  let activeTextFieldMaxCount = activeTextFieldType?.maxCharacter
            else {
                return false
            }
            if activeTextFieldType == .phone, let phoneNumber = formattingPhoneNumber(text: text, to: string, range: range) {
                textField.text = phoneNumber
                return false
            }
            let updatedText = text.replacingCharacters(in: stringRange, with: string)
            if activeTextFieldType == .postalCode {
                delegate?.updateAddressSearchButtonState(isEnabled: updatedText.count == activeTextFieldMaxCount)
            }
            return updatedText.count <= activeTextFieldMaxCount
        }
    
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            guard let activeType = activeTextFieldType else {
                return false
            }
            textField.endEditing(false)
            // Focus on the next field
            switch activeType {
            case .username:
                delegate?.setActiveTextField(.password)
            case .password:
                delegate?.setActiveTextField(.phone)
            case .phone:
                delegate?.setActiveTextField(.address)
            case .address:
                delegate?.setActiveTextField(.postalCode)
            case .postalCode:
                delegate?.setActiveTextField(.birthday)
            default:
                break
            }
            return false
        }
    }

    MockClass

    This class has a InputValidator and conforms to ProfileInputValidatorDelegate. It is use for XCTestCase.

    class MockClass {
        var activeTextField: UITextField?
        var searchButton = UIButton()
        var inputCheckType: ProfileInputType?
        var errorMessage: String?
    
        fileprivate let inputValidator = ProfileInputValidator()
        
        var usernameTextField: UITextField!
        var passwordTextField: UITextField!
        var addressTextField: UITextField!
        var phoneTextField: UITextField!
        var postalCodeTextField: UITextField!
        var birthdayTextField: UITextField!
        
        lazy var dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy.MM.dd"
            formatter.calendar = Calendar(identifier: .gregorian)
            return formatter
        }()
    
        init() {
            inputValidator.delegate = self
            inputValidator.dateFormatter = dateFormatter
    
            setupTextFields()
            searchButton.isEnabled = false
        }
    
        private func setupTextFields() {
            usernameTextField = UITextField()
            passwordTextField = UITextField()
            addressTextField = UITextField()
            phoneTextField = UITextField()
            postalCodeTextField = UITextField()
            birthdayTextField = UITextField()
        
            let textFields = [
                usernameTextField,
                passwordTextField,
    
                addressTextField,
                phoneTextField,
    
                postalCodeTextField,
                birthdayTextField
            ]
    
            for (index, textField) in textFields.enumerated() {
                textField?.delegate = inputValidator
                textField?.tag = index
            }
        }
    }
    
    extension MockClass: ProfileInputValidatorDelegate {
        func setActiveTextField(_ inputType: ProfileInputType) {
            switch inputType {
            case .username:
                usernameTextField.delegate?.textFieldDidBeginEditing?(usernameTextField)
            case .password:
                passwordTextField.delegate?.textFieldDidBeginEditing?(passwordTextField)
            case .address:
                addressTextField.delegate?.textFieldDidBeginEditing?(addressTextField)
            case .phone:
                phoneTextField.delegate?.textFieldDidBeginEditing?(phoneTextField)
            case .postalCode:
                postalCodeTextField.delegate?.textFieldDidBeginEditing?(postalCodeTextField)
            case .birthday:
                birthdayTextField.delegate?.textFieldDidBeginEditing?(birthdayTextField)
            }
        }
    
        func updateAddressSearchButtonState(isEnabled: Bool) {
            searchButton.isEnabled = isEnabled
        }
    
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?) {
            errorMessage = message
            inputCheckType = inputType
        }
    }

    Conclusion

    XCTest is a powerful framework for testing unit tests, but it can be challenging to test UI-related logic. In mobile development, UI logic is closely intertwined with the user interface itself. The conventional approach for testing UI logic is to use XCUITest. However, XCUITest tends to be slower compared to XCTest. If we have multiple views designed for registering or updating information using input fields, we would need to write multiple UI testing code with XCUITest.

    An alternative approach is to use XCTest and create a function that simulates user actions, such as typing text. This allows us to write a unit test for common input validator logic while considering user actions as well. This way, we can test the UI-related logic using XCTest in a more efficient manner compared to relying solely on XCUITest.

  • 애플 홈팟 미니 언박싱

    애플 홈팟 미니를 선물 받았다

    언박싱 사진들

    연결 방법

    간단하게 전원을 연결하고 아이폰을 근처로 가져다두면 팝업이 뜬다. 추가하기 버튼 누르면 애플 홈에 스피커가 추가된다

    음질 만족스럽고 디자인도 완전 맘에 든아

  • 싱가포르에서 보쌈 요리하기

    여기는 외식비가 비싸다. 레스토랑 같은 곳에서 먹으면 GST라고 음식값에 세금이 붙고 추가적으로 서비스 비용이 붙는 경우도 있다.

    그래서 실제 결제 금액은 메뉴판에 있는 금액 보다 더 붙는다

    아무튼 비싼 외식 값 때문에 집에서 자주 요리를 한다. 오늘은 보쌈 만드는 방법 간략하게 정리

    참고로 아래 조리법은 백종원 유투브 그대로 따라한 것이다.

    삼겹살 구매하기

    어디서 사도 상관은 없는데 나는 주로 Fair Price 슈퍼의 정육점 코너로 가서 1인분 인 경우 주로 150-200 그램 (가격은 약 5-7달러), 2인분인 경우 300그램 호주산 삼겹살 포크벨리를 구매한다.

    그외 준비물

    양파 반조각

    된장 2 스푼

    사진이 좀 흔들렸다. 아무튼 삼겹살이 잠길정도로 물을 붓고 양파랑 파 그리고 된장 넣고 한 30-45분 끓인다

    소스 만들기 (옵션)

    물이 끓는 동안 소스를 만든다. 일단 새우젓이 필요한데 이건 한인마트인 고려마트에서 구매했다. 그리고 고추가루는 국산 강추. 이것도 고려마트에서 팔지만 나는 장모님이 주신거 가져왔다

    준비물

    새우젓 크게 2-3 스푼

    청양고추 잘게 썰기

    한국산 고추가루 1스푼

    설탕 두스푼

    간마늘 (이건 Fair Price에서 중국산으로 구매)

    완성

    진짜 보쌈은 요리하는거 엄청 쉽다. 한번 도전해보는 것을 강력추천한다

    나는 특히 새우젓 소스랑 같이 먹는 것이 너무 맛있었다. (백종원 유투브 최고)

    아 그리고 보쌈 끓이고 남은 물 버리지말고 두부 사다가 넣어먹으면 아주 훌륭한 된장찌게가 된다. (육수국물이기 때문에 맛있다)

    반응 좋아요 많으면 제육볶음 등 다른 요리도 올릴 예정

  • Udemy 헬스 강의 정리 – 식단 편

    Udemy 헬스 강의 정리 – 식단 편

    예전에 구매했던 강의를 다시 보고 있다. 운동을 좀 다시 체계적으로 해보려고 한다.

    https://www.udemy.com/course/build-muscle-without-a-gym-science-based-bodyweight-workout/learn/lecture/14016062#overview

    아무튼 Udemy에서 헬스 관련 쪽으로 거의 탑인 강사가 알려주는 팁을 정리해보고자 한다.

    필수 영양 요소

    탄수화물 Carbohydrate

    • 야채, 오트밀, 브라운 라이스, 퀴노아, 통곡물, 감자

    프로틴 (근육생성)

    • 생선, 저지방 치킨, 저지방 소고기, 요거트, 우유, 계란, 견과류

    지방 (Fats)

    • 아보카도, 계란, 올리브 오일 견과류, 코코넛 오일, 다크 초콜릿

    근육 생성 공식 (40% 탄수화물, 30% 프로틴, 30% 지방 섭취)

    • 여기서 살을 빼려면 칼로리를 줄이고 프로틴 섭취량 늘리기

    운동 전후 음식 (전후 30분 섭취 권장)

    • 오트밀(탄수화물과 프로틴이 모두 있다고 함) + 프로틴 쉐이크 추천
    • 바나나도 추천

    그 외에 추천

    • 프로틴 파우더
    • 크레아틴 Creatine (소고기 / 생선) -> 근데 이건 보통 보충제로 섭취한다고 함
    • 생선 오일 (오메가3 먹으란 소리인가)

    섭취 비율

    프로틴

    • 1.76 – 2.2 그램을 몸무게 1KG 단위로 먹으라고 함. 자기 몸무게 곱하기 1.76 – 2.2 그램 만큼 섭취하라는 소리

    지방

    • 하루 칼로리의 15-35 프로 섭취

    탄수화물

    • 2.2 – 3.85 그램을 몸무게 1KG 단위로 섭취

    까먹을까봐 정리하는 유데미 강의 노트 정리