베이직반 세션 1차 목표
Swift와 익숙해지는 것이 가장 중요!
제 2의 외국어라고 생각하는 것.
코드를 많이쳐서 익숙해 질 것.
문제 잘게 쪼개기 ✂️✂️✂️
앱스토어 - Swift Playgrounds 앱 설치
Get Started with Code, Learn to Code 1, 2 진행
위 프로그램은 게임 형태로 Swift를 배울 수 있는 도구가 있습니다.
한번씩 접해서 게임을 통해 코드에 대한 이해를 높여가는 방법 또한 좋습니다.
IPad의 playground와 같다고 보시면 됩니다.
본격적인 Swift 공부에 앞서 Swift 코드를 테스트 해볼 수 있는
사이트를 하나 추천해 드리겠습니다.
Xocde로 플레이그라운드를 사용하셔도 무관합니다.
https://www.mycompiler.io/ko/new/swift
새 Swift 프로그램 만들기 - 마이컴파일러 - myCompiler
실행 코드 코드 저장 기존 코드를 유지하시겠습니까? 에디터에 코드가 있는 동안 언어를 전환하려고 합니다. 이를 유지하려면 “기존 코드 유지”를 선택합니다. 예제로 바꾸려면 “예제로 바
www.mycompiler.io
라인단위 : 연속된 생성자
let digit: Characater = "9"
let intDigit = Int(String(digit))
위 Swift 코드를 분석해 봅시다.
"9"라는 문자열 리터럴을 "digit" 이라는 상수에 넣고 있습니다.
지금 위에서 굵은 글씨 부분들로 강조를 해주고 있는데, 위 강조된 키워드를
자신이 제대로 알고 넘어가는 것이 타 개발자 분들과의 커뮤니케이션
그리고
자료들을 볼 때에 "한글 블로그" or "WWDC" or "영어블로그 등"을 볼때 키워드를
정확하게 짚어 줄 수 있는것이 중요합니다.
값 그차체를 가르키는 것 → 리터럴 이라고 부릅니다.
때문에
위에서 보이는 문자열 '9' 는 리터럴 이라고 부룰 수 있습니다.
이러한 문자열 리터럴 "9"를 "digit" 이라는 상수에 넣어주는데,
위에서는 임의 적으로 타입이 명시되어 있죠 "Characater" 라고 말이죠
일반적으로는
let digit = "9"
이런식으로 넣을 수 있지만, 이러한 경우의 "digit"은 어떤 타입이 되는 걸까요?
(생각해보기)
String 타입이되는 것이죠?
여기서
아래와 같이 같은 형식의 "Characater 타입"을 명시를 해준다면
let digit: Characater = "9"
"digit"은 "Characater 타입"으로 저장이 되는 것 입니다.
단, 한글자 길이의 문자열이여야 합니다.
다음으로
let digit: Characater = "9"
이 상수를 어떠한 생성자를 여러번 씌워서 아래 와 같이 "intDigit" 이라는 값으로 넣어줍니다.
let intDigit = Int(String(digit))
위 처럼 생성자를 여러개가 연결 되어 있을 때 햇갈리는 부분들이 있는데,
위처럼 "Int(String(digit))" 이런식으로 되어있는 값들을
쉽게 말해서 "익스프레션(expression)"이라고 부릅니다.
즉, 어떠한 표현식-값으로 연산이되는 이러한 라인을 보기 위해서는
let intDigit = Int(String(digit))
우선 위 코드를 쪼개서 생각해보아야 합니다.
왼쪽에서 오른쪽으로 흘러간다고 보시면 됩니다.
왼쪽에서 읽어보면 digit 이라는 것이 String 선언되어 있고
String 으로 받은 digit이 다시 정수인 Int 로 받아서 그것을 intDigit 이라는 Let 상수로
받고 있는 것 이렇게 설명 한다면 뭔소리인지 하나도 모를 수 있습니다.
조금 더 알기 쉽게 쪼개 보는 방식이 있습니다.
코드를 읽어 보는 방식 자체는 왼쪽부터이며,
쪼개는 방식은 아래와 같이 쪼개서 읽어 주면,
조금 더 알기 쉽습니다.
Let | intDigit | = | Int(String(digit))
← ← ← →
이렇게 쪼개서 왼쪽부터 읽어 보면 됩니다.
이제 쪼개보았으니 분석을 해봅시다.
Int 인데 괄호 안에 있는 어떠한 값을
"Int 타입의 어떠한 새로운 값으로 만드려고 하는 구나" 까지 보고
그 안에 String 으로 시작하는 어떠한 생성자가 있죠?
"String 생성자를 보고 괄호가 열리고 닫힌것을 보기" 까지 봅니다.
어떠한 값이 들어간다 해서 "= (등호)(대입연산자)(할당연산자)"가 들어가며,
intDigit 이라는 이름의 Let 상수에 대입, 할당이 된다는 의미가 됩니다.
자 여기까지 봤을때 전체 코드를 보면
let digit: Characater = "9"
let intDigit = Int(String(digit))
Digit 이라는 Characater 타입의 값을 String 이라는 새로운 값으로 생성해서
그렇게 생성된 새로운 String 값을 Int 라고 하는 새로운 값으로 만들어주는것입니다.
조금 더 알아보기
(위 코드와 이어지는 문제)
let strNumber = String(digit)
let intNumber = Int(strNumber)
이 코드도 한번 쪼개 보고 읽어보세요.
let strNumber = String(digit) // "9"
String 이라는 생성자에 Digit 을 넣어주고 있죠
그러면 상수 strNumber에 할당이 되는 것이겠죠?
String 은 문자열이니까 위에서의 값은
문자열인 "9" 가 되는 것 입니다.
let intNumber = Int(strNumber) // 9
그런데 이러한 문자열 "9"를
다시 Int 라는 생성자 안에 넣어주고 있죠?
그래서 새로운 int의 값을 만들고 있습니다.
그러면 이부분의 값은 정수형인 9 가 되는 것 입니다.
코드가 길어 지더라 하더라도 "하나 하나 쪼개서 봐야 하는것"이 "핵심 포인트" 입니다.
라인단위 : 3항 연산자
Swift에서 삼항연산자를 소개한다면 쉽게 말해서
"if else"를 간단하게 "3항연산자"로 표현할 수 있습니다.
/// A 가 클 경우 빨강
/// B 가 클 경우 파랑
let a = 3
let b = 5
let isAGreater: UIColor = a > b ? .red : .blue
이 코드를 분석해 보면
a가 b보다 큰것인지 물어보고 있고,
true(참) 일 경우 red가 False(거짓) 일 경우 blue가
isAGreater 이라는 상수에 UI 컬러가 들어가는 것입니다.
그래서 현재 a가 b보다 작아보이죠?
그러면 위 코드의 삼항연산자의 전체 코드의 값은
결국엔 blue 로 판단이 되는 것입니다.
라인단위 : for-in
let name = "Jun"
for char in name {
print(char)
}
이 코드의 값은 어떻게 될까요?
위의 값은 프린트가 3번 되겠죠?
왜? : 3개의 문자이기 때문입니다.
J
u
n
그러니까 왜..?
기본적으로 Swift의 Print 함수에는 terminator 이라는 변수가 들어있는데
terminator는 무엇이냐? "\n" (줄바꿈 또는 개행문자) 입니다.
그렇기 때문에 총 3개의 프린트가 이루어지는것입니다.
이를 토대로 코드를 써보면
J\n
u\n
n\n
이렇게 이루어 진다고 보면 됩니다.
(돌발질문)
"char" 이라는 것은 무슨타입인가요?
파일 단위
스위프트는 기본적으로 ↑(위) 에서 ↓(아래)로 실행됩니다.
(코드가 위에서 한줄 한줄씩 실행 되는것을 말합니다.)
물론 스위프트 뿐만 아닌 다른 코드들도 이는 동일 합니다.
스위프트는 호출하는 코드와 정의하는 코드를 구분 합니다.
(여기서 정의하는 코드는 결과를 바로 나타내지 않습니다.)
func sayHello() {
print("Hello, World!")
}
이 코드의 경우 함수를 정의 하고 있습니다.
어떤 결과가 나올것 같나요?
네, 아무것도 나오지 않아요.
왜 그런것인가? -> 이 코드의 경우 단순히 함수를 정의하는 코드이고
결과를 보고 싶으면 호출을 해야만 결과를 받아 볼 수 있는것입니다.
조금더 쉽게 이해를 돕기 위해서 설명 드리겠습니다.
밥을 먹기 위해서 식기와 음식을 준비 했는데,
"먹어도 된다"라는 말이 없는 상태인 것 입니다.

그렇다면,
위 함수의 결과를 보기 위해서는 호출을 해야겠죠?
func sayHello() {
print("Hello, World!")
}
sayHello()
함수를 호출하기 위해서는 정의된 함수 이름을 그대로 써서 괄호 열고 닫아주시면 됩니다.
즉, "함수는 호출해야 함수 바디 코드가 실행된다!" 라고 기억 해두시면 됩니다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
이 Swift 코드는 함수가 아니라 클래스죠?
Swift 클래스도 마찬가지로 정의하는 코드와 호출하는 코드가 구분되어 있습니다.
위 클래스 코드도 현재 정의만 되어 있습니다.
프로젝트 단위
Swift의 프로젝트 단위는 정말 중요한 부분입니다.
무슨 파일, 무슨 코드부터 실행될까?
Swift Command Line Tool (계산기, 숫자야구 프로젝트) 컴파일러가
main.swift 를 프로그램 시작점으로 인식하죠.
컴파일러 = 코드를 프로그램으로 만드는 과정 중 사용되는 도구 중 하나 입니다.
Xcode에서 iOS App 프로젝트에는 main.swift는 없습니다.
그런데 시뮬레이터는 실행이 되죠?
어째서인가?
호출하는 코드를 넣은 적이 없는데 왜 실행이 되는 것인가?
이제부터 중요합니다.
가장먼저 App를 만들기 위해 스토리보드로 앱 프로젝트 파일을 생성하면,
AppDelegate와 SceneDelegate를 볼 수 있는데,
여기서 import UIKit 이라는 것을 볼 수 있어요
이 UIKit 이라는 프레임워크 (좀더 넓게 봐서 "Cocoa Touch" 라는 프레임워크)는
기본적으로 AppDelegate 안에 main 이라는 함수를 숨겨놓습니다.
숨겨놓다기 보다는 기본적으로 제공한다 가 맞는 말이겠죠?
이미지에서 보면
AppDelegate 라는 클래스가 있는데,
이 AppDelegate 가 UIApplicationDelegate의 프로토콜을 채택하고 있는 것 입니다.
그렇다면 프로토콜을 채택한다는 개념은 무엇일까요?
AppDelegate 클래스에 UIResponder, UIApplicationDelegate 가 나란히 있죠?
이런것을 프로토콜을 채택한다고 합니다.
프로토콜의 기능중의 하나를 설명한다면 프로토콜을 채택만 한다면
어떠한 함수를 기본으로 제공받을 수 있는 것 입니다.
즉, AppDelegate가 UIApplicationDelegate 라는 프로토콜을 채택을 하고 있는데,
UIApplicationDelegate 라는 프로토콜은 main이라는 함수를 기본으로 제공하고 있는 것 입니다.
Xcode에서 iOS App 프로젝트는 어떤 파일이 가장 먼저 실행될까요?
@main 이라고 지정한 타입의 static func main() 이 먼저 실행이 됩니다.
만약, 앱이 실행되기 직전에 뭔가를 하고 싶다면,
AppDelegate 에 코드를 작성하면
시스템이 앱을 실행하기 완전 직전에 그 코드를 실행하게 됩니다.
AppDelegate와 SceneDelegate에 대해서는
자세하게 다루기엔 조금 글이 많이 길어질 수 있어서
따로 나눠서 나중에 글을 쓰도록 하겠습니다.
Xcode 와 친해지기 (feat. UIKit Framework)
디버그 영역
디버그 영역은 내 코드가 실패한것과 성공한것 등등
코드에 결과에 대해서 쉽게 알 수 있는 영역을 이야기 합니다.
디버그 영역에서는 다양한 툴이 있는데, 그중 가장 많이 사용하는 것은
브레이크 포인트입니다.
실행 중 일시정지 하여 현재 값들을 살펴볼 수 있고,
라인 넘버를 선택해서 활성화/비활성화 가 가능하며
브레이크 포인트를 드래그해서 이동과 삭제가 가능 합니다.
다음으로 많이 쓰는 툴로 Xcode에서는 lldb 디버그를 사용하는데,
이렇게 브레이크 포인트를 걸어서 14번째까지의
맥락(실행시점)에 접근 해서 값을 볼 수 있습니다.
lldb 명령어에서는 딱 한가지만 많이 쓰입니다.
"po" 라는 lldb 명령어 인데,
만약 "po" 라는 명령어가 생각이 안나면 help 를 입력해서 알아낼 수 있어요.
이렇게 메모리 주소도 볼 수 있고 그 타입도 볼 수 있어요
이런식으로 디버깅 영역에서 많은 일들을 할 수 있습니다.
물론 print 를 사용해서 값을 볼 수 있기도 해요.
인스펙터 영역, 네비게이터 영역
이러한 부분들은
Xcode 개발 도구의 구성요소와 영역들
Xcode의 구성 요소와 영역들을 알아봅시다!Xcode 각 부분과 명칭프로젝트를 실행하거나 생성된 프로젝트를 실행하면 Xcode는 여러 개의 작은 영역으로 분할된 커다란 윈도우 형태로 나타납니다. '워
stayjun.tistory.com
Xcode 개발 구성요소와 영역들을 살펴 보시면 되겠습니다.
Function & Clousure
Function = named closure
동작, 행동
쉽게 말해서 Function 은 어떠한 input을 넣으면 output을 뱉어내는 것을 말하는데,
빵을 만들기 위해서는 무엇을 해야할까요?
밀가루를 빵 만드는 기계에 넣어서 빵을 완성 시키는 원리를 이야기 합니다.
(필자의 경우 이 부분을 뒤늦게 알았지만, 여기저기 떠돌아다니는 Function이 들어간 코드를 많이 해석 해 보고 이 그림을 보면 이해가 됩니다.)
함수(Function) 은 클래스 내부에 정의 되거나,
class나 struct (구조체), enum (열거형) 이러한 곳에 정의가 된다면
method 라는 이름으로(이름만) 바뀌게 됩니다.
물론, 위 그림과 같은 원리를 가르키긴합니다.
기본적으로는 instance가 가지는 method가 되어서
구체적으로는 instance method 가 되는 것 입니다.
class a {
func b() {
}
}
let aa = a()
aa.b
쉽게 말해서 instance method 는 클래스의 범위안에
함수 (Function)을 정의 하는데,
클래스 범위 안에 함수를 만들어서 정의하고 그것을 인스턴스로 만들어서(let aa = a())
aa 라는 인스턴스에 접근(.)해서 b라는 생성자를 호출 해주는 이러한 method 가 되고 있는 것입니다.
다른종류의 method 도 있나요?
네, 있습니다.
class a {
func b() {
}
static func c(){
}
}
let aa = a()
aa.b() // 인스턴스 메서드
aa.c() // 이런식으로 접근은 불가 타입이름으로 접근이 가능 ex) a.c()
a.c()
static 의 경우는 a 라는 타입 그 자체가 가지는 method 로 정의를 하겠다는 것 입니다.
static이 아니고 class 로도 가능한데 중요하지는 않습니다.
클래스 타입에 정의된 함수 = type method → static, class 키워드 사용
Argument & Parameter
Argument : 호출 입장에서 전달하는 값
Parameter : 호출 당하는 함수 입장에서 전달 받는 값
둘은 같은 것 입니다.
쉽게 설명해 보자면...
파라미터(Parameter)
파라미터는 함수나 메서드를 정의할 때,
그 함수나 메서드가 필요로 하는 입력 값을 의미하는데,
예를 들어, 쉽게 생각해서 케이크를 만들 때 필요한 재료라고 생각하면 됩니다.
케이크를 만들기 위해 밀가루, 설탕, 달걀 등이 필요하잖아요?
마찬가지로 함수도 어떤 작업을 하기 위해 필요한 재료가 있는데,
그게 바로 파라미터 입니다.
func addNumbers(a: Int, b: Int) -> Int {
return a + b
}
이 코드에서 a와 b가 파라미터입니다.
이 함수는 두 개의 숫자를 더하기 위해
a와 b라는 재료가 필요한 것이죠
그렇다면,
Argument(아규먼트)는 무엇인가?
아규먼트(Argument)
아규먼트는 함수를 호출할 때 실제로 전달하는 값입니다.
다시 말해, 파라미터에 실제로 들어가는 값을 말하는데,
func addNumbers(a: Int, b: Int) -> Int {
return a + b
}
let sum = addNumbers(a: 5, b: 3)
코드를 보면 5와 3이 아규먼트인 것 입니다.
이 값들이 함수의 파라미터 a와 b로 전달됩니다.
Argument label & Parameter Name
아규먼트 레이블(Argument Label) 아규먼트 레이블은 함수 호출 시
각 파라미터의 역할을 명확히 하기 위해 사용되는 이름인데,
아규먼트 레이블은 함수를 정의할 때 지정하며,
함수 호출 시에는 아규먼트 앞에 붙여서 사용합니다.
func addNumbers(firstNumber a: Int, secondNumber b: Int) -> Int {
return a + b
}
let sum = addNumbers(firstNumber: 5, secondNumber: 3)
이 코드에서 firstNumber와 secondNumber가 아규먼트 레이블입니다.
firstNumber: 5와 secondNumber: 3이 함수 호출 시 사용되며,
5와 3이 아규먼트 입니다.
아규먼트: 함수 호출 시 전달되는 실제 값을 의미하고
아규먼트 레이블은 함수 호출 시 각 파라미터의 역할을 명확히 하기 위해 사용되는 이름을 말합니다.
func greet(name: String) {
print("Hello, \(name)!")
}
greet(name: "Alice")
"name"은 파라미터
"Alice"는 아규먼트
이렇게 보면 좀더 쉽게 이해가 될까요?
아규먼트 레이블을 사용하는 함수
func greet(person name: String) {
print("Hello, \(name)!")
}
greet(person: "Alice")
"person"은 아규먼트 레이블
"name"은 파라미터
"Alice"는 아규먼트
이렇게 보면 알기 쉽습니다.
Parameter Name(파라미터 이름)은 함수나 메서드를 정의할 때 사용되는 변수 이름을 말하고
Parameter Name(파라미터 이름)은 함수 내부에서 사용되며,
전달된 값을 참조하는 데 사용됩니다.
파라미터 이름은 아규먼트 레이블과 다르지만,
때로는 같은 이름을 사용할 수도 있습니다.
파라미터 이름과 아규먼트 레이블이 같은 경우
func greet(name: String) {
print("Hello, \(name)!")
}
greet(name: "Alice")
파라미터 이름 = "name "
아규먼트 레이블 = "name "
아규먼트: "Alice"
파라미터 이름과 아규먼트 레이블이 다른 경우
func greet(person name: String) {
print("Hello, \(name)!")
}
greet(person: "Alice")
파라미터 이름: "name"
아규먼트 레이블: "person"
아규먼트: "Alice"
아규먼트 레이블을 생략하고 파라미터 이름만 사용할 수도 있습니다.
이를 위해 아규먼트 레이블을 "_"(언더바)로 표시합니다.
func greet(_ name: String) {
print("Hello, \(name)!")
}
greet("Alice")
파라미터 이름: "name"
아규먼트 레이블: "없음(생략)"
아규먼트: "Alice"
Closure (동작, 행동)(클로저)
function도 클로저 인데 이름이 있는 클로저라고 합니다.
클로저는 코드를 쉽게 말해서 간단하게 저장하고 나중에 실행할 수 있는 방법을 말하는데,
마치 함수를 변수에 저장하는 것과 비슷합니다.
클로저를 더 쉽게 비유로 설명 하자면,
클로저는 마치 레시피 노트와 같은데,
레시피를 노트에 적어두고 나중에 필요할 때 그 레시피를 꺼내서 요리 하는겁니다.
여기서 레시피 노트가 클로저이고, 레시피는 코드인 부분입니다.
노트에 적힌 레시피를 보고 요리를 할 때, 요리를 실행한다고 말할 수 있는데,
클로저도 마찬가지로, 저장된 코드를 나중에 실행할 수 있죠
unnamed closure & named closure
이름이 있는 클로저 (Named Closure)
이름이 있는 클로저는 우리가 자주 사용하는 함수와 같은데,
함수는 이름이 있고, 그 이름을 사용해서 언제든지 호출할 수 있는것을 말합니다.
named closure = function(함수) 이고,
Swift 에서는 클로저를 변수/상수에 저장, 파라미터에 전달, 리턴값으로 반환이 가능 합니다.
func sayHello() {
print("Hello!")
}
sayHello() // 출력: Hello!
sayHello 라는 이름이 있는 함수(클로저) 입니다.
언제든지 sayHello()를 호출해서 "Hello!"를 출력할 수 있습니다.
이름이 없는 클로저 (Unnamed Closure 또는 익명 클로저)
이름이 없는 클로저는 함수처럼 사용할 수 있지만,
이름이 없어서 바로 작성하고 사용합니다.
이름이 없기 때문에 한 번 쓰고 버리는 경우가 많습니다.
배열을 정렬할 때도 이름이 없는 클로저를 많이 사용하는데 아래 코드를 보면,
let numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5]
let sortedNumbers = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
return a < b
})
print(sortedNumbers) // 출력: [1, 1, 2, 3, 4, 5, 5, 6, 9]
{ (a: Int, b: Int) -> Bool in return a < b }는 이름이 없는 클로저이고,
이 클로저를 sorted 함수에 직접 전달해서 사용했죠
네이밍 컨벤션
스위프트는 기본적으로 카멜케이스를 사용
camelCase
변수에 값을 저장하는 코드 뒤에서 일어나는 일
...
var name = "손흥민"
...
컴파일 타임에 스택영역에 필요한 메모리 공간이 계산되고
프로그램이 실행될 때, 계산된 메모리 공간만큼 스택 영역이 할당됩니다.
변수에 저장하는 코드가 실행될 때, 메모리 공간에 값이 초기화됩니다.
변수와 상수의 차이
값이 초기화 된 이후 변수의 스택영역 메모리 공간에 대한 변경 가능 여부
var name = "손흥민"
name = "대상혁" // O.K
let name = "이강인"
name = "최우제" // Complie Error
변수의 변경가능함 은 Mutable 이라고도 표현하고
변경 불가능함은 Immutable 이라고 표현합니다.
스코프
변수가 선언된 위치가 스코프를 결정합니다.
저장된 값이 유효한 범위
전역변수 = 프로그램 어디서나 접근할 수 있는 변수이며,
지역변수 = 함수 내부를 말합니다.
class/ struct 에 정의된 변수/상수는
property, variable, state 라고도 표현합니다.
고민거리
상수는 저장된 값의 변경이 불가능하다고 했는데..
아래 코드에서는 왜 컴파일 에러가 발생하지 않는 이유가 무엇인가요?
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let legend = Person(name: "손흥민", age: 31)
legend.age = 40 // O.K
클래스 인스턴스는 참조 타입이기 때문에 클래스 인스턴스를 변수나 상수에 할당할 때,
그 인스턴스 자체가 아닌 인스턴스의 참조(주소)가 할당된다는 의미이고,
let 키워드는 상수(constant)를 정의할 때 사용되는데,
상수로 정의된 클래스 인스턴스의 속성은 여전히 변경할 수 있기 때문이죠,
let 키워드가 클래스 인스턴스의 참조가 변경되지 않음을 의미할 뿐,
인스턴스의 속성 자체를 변경할 수 없음을 의미하지 않기 때문이라고 하면 더 알기 쉽죠?
함수의 매개변수(parameters) 는 var 일까? let 일까?
Swift에서 함수의 매개변수(parameters)는 기본적으로 let으로 취급되는데,
이는 매개변수가 함수 내부에서 변경될 수 없음을 의미하는 것입니다.
'IOS > Swift-Study' 카테고리의 다른 글
[Swift-Study] iOS 앱 개발 숙련 1주차 - 메모리&ARC 개념 (0) | 2024.04.12 |
---|---|
[Swift-Study] 베이직반 2회차 - 1회차 복습, Closure 활용 VC 간 통신 (0) | 2024.04.11 |
[Swift-Study] IOS 앱 개발 입문 1주차 - 간단 디버깅 (0) | 2024.04.02 |
[Swift-Study] IOS 앱 개발 입문 1주차 - 로직다루기 (0) | 2024.04.02 |
[Swift-Study] IOS 앱 개발 입문 1주차 - LifeCycle (0) | 2024.04.02 |