Ver3.0 ๐ iOS ๊ตญ์ ํ์ NSLocalizedString์ ํ์ฉํ ๋ค๊ตญ์ด ์ง์

๐ iOS ๊ตญ์ ํ์ NSLocalizedString์ ํ์ฉํ ๋ค๊ตญ์ด ์ง์
์ ์ธ๊ณ ์ฌ์ฉ์์ ์ํตํ๋ ์ฑ ๋ง๋ค๊ธฐ ๐
๐ฏ ์ ๊ตญ์ ํ๊ฐ ์ค์ํ ๊น?
์๋
! ์ค๋์ iOS ์ฑ ๊ฐ๋ฐ์์ ์ ๋ง ์ค์ํ ์ฃผ์ ์ธ ๊ตญ์ ํ(Internationalization, ์ค์ฌ์ i18n)์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ๐
ํน์ ์ฑ์ ๋ง๋ค๋ฉด์ "์ฐ๋ฆฌ ์ฑ์ ์ ์ธ๊ณ ์ฌ๋๋ค์ด ์ฌ์ฉํ๋ฉด ์ข๊ฒ ๋ค"๋ ์๊ฐ ํด๋ณธ ์ ์์ด? ์๋๋ฉด "ํ๊ตญ์ด๋ก๋ง ๋ง๋ค์๋๋ฐ ์ธ๊ตญ์ธ ์น๊ตฌ๊ฐ ์ฐ๊ธฐ ํ๋ค๋ค๊ณ ํ๋๋ผ"๋ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์? ๋ฐ๋ก ์ด๋ด ๋ ํ์ํ ๊ฒ ๊ตญ์ ํ์ผ! ๐
๊ตญ์ ํ๋ ๋จ์ํ ์ธ์ด๋ฅผ ๋ฒ์ญํ๋ ๊ฒ ์ด์์ ์๋ฏธ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ ์ง ํ์, ํตํ ํ์, ์ซ์ ํฌ๋งท, ์ฌ์ง์ด ํ
์คํธ๊ฐ ํ์๋๋ ๋ฐฉํฅ(์๋์ด๋ ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก!)๊น์ง ๋ชจ๋ ๊ณ ๋ คํด์ผ ํ๊ฑฐ๋ . ๐ค
์ง์ญํ(l10n): ํน์ ์ธ์ด์ ์ง์ญ์ ๋ง๊ฒ ์ค์ ๋ก ๋ฒ์ญํ๊ณ ์กฐ์ ํ๋ ๊ณผ์
์ฝ๊ฒ ๋งํ๋ฉด, ๊ตญ์ ํ๋ "์ค๋น ์์ "์ด๊ณ ์ง์ญํ๋ "์ค์ ์ ์ฉ"์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ! ๐
App Store ํต๊ณ๋ฅผ ๋ณด๋ฉด ์ ๋ง ๋๋ผ์ด ์ฌ์ค์ ์ ์ ์์ด. ๋ค๊ตญ์ด๋ฅผ ์ง์ํ๋ ์ฑ์ ๊ทธ๋ ์ง ์์ ์ฑ๋ณด๋ค ํ๊ท ์ ์ผ๋ก 128% ๋ ๋ง์ ๋ค์ด๋ก๋๋ฅผ ๊ธฐ๋กํ๋ค๊ณ ํด. ๐ฒ
ํนํ ์ฌ๋ฅ๋ท(https://www.jaenung.net) ๊ฐ์ ํ๋ซํผ์์ ์ฑ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ํ๋ก์ ํธ๋ฅผ ์งํํ ๋, ๊ตญ์ ํ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ค๋ฉด ํจ์ฌ ๋ ๋์ ํ๊ฐ๋ฅผ ๋ฐ์ ์ ์์ด. ๊ธ๋ก๋ฒ ์์ฅ์ ํ๊ฒ์ผ๋ก ํ๋ ํด๋ผ์ด์ธํธ๋ค์๊ฒ๋ ํ์ ๊ธฐ๋ฅ์ด๋๊น! ๐ผ
๐ NSLocalizedString์ ๊ธฐ๋ณธ ๊ฐ๋
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Swift์์ ๋ค๊ตญ์ด๋ฅผ ์ง์ํ๋ ํต์ฌ ๋๊ตฌ์ธ NSLocalizedString์ ๋ํด ์์๋ณผ๊น? ๐
NSLocalizedString์ Foundation ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ๋งคํฌ๋ก ํจ์์ผ. ์ด ์น๊ตฌ์ ์ญํ ์ ์์ฃผ ๊ฐ๋จํด. ์ฝ๋์์ ํ๋์ฝ๋ฉ๋ ๋ฌธ์์ด ๋์ , ์ฌ์ฉ์์ ์ธ์ด ์ค์ ์ ๋ง๋ ๋ฒ์ญ๋ ๋ฌธ์์ด์ ๊ฐ์ ธ์ค๋ ๊ฑฐ์ง! โจ
// ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ํํ
let greeting = NSLocalizedString("greeting", comment: "์ธ์ฌ๋ง")
// ์ค์ ์ฌ์ฉ ์์
label.text = NSLocalizedString("welcome_message", comment: "ํ์ ๋ฉ์์ง")
button.setTitle(NSLocalizedString("login_button", comment: "๋ก๊ทธ์ธ ๋ฒํผ"), for: .normal)
์ฌ๊ธฐ์ ์ค์ํ ํฌ์ธํธ๊ฐ ๋ช ๊ฐ์ง ์์ด! ๐
1. Key ๊ฐ: ์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ "ํค(key)" ์ญํ ์ ํด. ์ด ํค๋ฅผ ํตํด ์ค์ ๋ฒ์ญ๋ ๋ฌธ์์ด์ ์ฐพ์์ค๋ ๊ฑฐ์ผ.
2. Comment: ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ ๋ฒ์ญ๊ฐ๋ค์ ์ํ ์ค๋ช
์ด์ผ. "์ด ๋ฌธ์์ด์ด ์ด๋์ ์ฐ์ด๋์ง" ์๋ ค์ฃผ๋ ๊ฑฐ์ง. ๋์ค์ ๋ฒ์ญ ์์
ํ ๋ ์์ฒญ ์ ์ฉํด! ๐
3. ์๋ ์ธ์ด ๊ฐ์ง: ์ฌ์ฉ์์ ๊ธฐ๊ธฐ ์ธ์ด ์ค์ ์ ์๋์ผ๋ก ๊ฐ์งํด์ ์ ์ ํ ๋ฒ์ญ์ ๋ณด์ฌ์ค. ๊ฐ๋ฐ์๊ฐ ๋ฐ๋ก ์ธ์ด๋ฅผ ์ฒดํฌํ ํ์๊ฐ ์๋ค๋ ๊ฒ ์ ๋ง ํธ๋ฆฌํ์ง! ๐
๐ฆ NSLocalizedString์ ๋ค์ํ ๋ณํ๋ค
NSLocalizedString์๋ ์ฌ๋ฌ ๊ฐ์ง ๋ณํ ํจ์๋ค์ด ์์ด. ์ํฉ์ ๋ฐ๋ผ ์ ์ ํ ๊ฑธ ์ ํํด์ ์ฐ๋ฉด ๋ผ! ๐จ
// 1. ๊ธฐ๋ณธ NSLocalizedString
NSLocalizedString("key", comment: "์ค๋ช
")
// 2. ํน์ ํ
์ด๋ธ ํ์ผ ์ง์
NSLocalizedString("key", tableName: "CustomStrings", comment: "์ค๋ช
")
// 3. ํน์ ๋ฒ๋ค ์ง์
NSLocalizedString("key", bundle: .main, comment: "์ค๋ช
")
// 4. ๋ชจ๋ ์ต์
์ง์
NSLocalizedString("key",
tableName: "Localizable",
bundle: .main,
value: "๊ธฐ๋ณธ๊ฐ",
comment: "์ค๋ช
")
// 5. ๋ณต์ํ ์ฒ๋ฆฌ๋ฅผ ์ํ ํน๋ณํ ํจ์
let format = NSLocalizedString("items_count", comment: "์์ดํ
๊ฐ์")
let message = String.localizedStringWithFormat(format, itemCount)
๊ฐ๊ฐ์ ํ๋ผ๋ฏธํฐ๊ฐ ๋ญ ์๋ฏธํ๋์ง ์์ธํ ๋ณผ๊น? ๐
| ํ๋ผ๋ฏธํฐ | ์ค๋ช | ๊ธฐ๋ณธ๊ฐ |
|---|---|---|
| key | ๋ฒ์ญ ๋ฌธ์์ด์ ์ฐพ๊ธฐ ์ํ ๊ณ ์ ์๋ณ์ | ํ์ |
| tableName | ๋ฌธ์์ด์ด ์ ์ฅ๋ .strings ํ์ผ ์ด๋ฆ | "Localizable" |
| bundle | ๋ฌธ์์ด ํ์ผ์ด ์๋ ๋ฒ๋ค | .main |
| value | ๋ฒ์ญ์ ์ฐพ์ง ๋ชปํ์ ๋ ์ฌ์ฉํ ๊ธฐ๋ณธ๊ฐ | key ๊ฐ |
| comment | ๋ฒ์ญ๊ฐ๋ฅผ ์ํ ์ค๋ช | ํ์ |
๐ Localizable.strings ํ์ผ ๋ง๋ค๊ธฐ
์, ์ด์ ์ค์ ๋ก ๋ฒ์ญ ํ์ผ์ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! NSLocalizedString์ด ์ฐธ์กฐํ๋ ์ค์ ๋ฒ์ญ ๋ฐ์ดํฐ๋ Localizable.strings ํ์ผ์ ์ ์ฅ๋ผ. ๐
์ด ํ์ผ์ ๋ง๋๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์์ธํ ์์๋ณด์! ๐ถโโ๏ธ
Xcode์์ File โ New โ File... ์ ํ
Resource ์น์ ์์ "Strings File" ์ ํ
ํ์ผ ์ด๋ฆ์ ์ ํํ "Localizable"๋ก ์ ๋ ฅ (๋์๋ฌธ์ ๊ตฌ๋ถ!)
ํ๋ก์ ํธ์ ์ ์ ํ ์์น์ ์ ์ฅ
ํ๋ก์ ํธ ๋ค๋น๊ฒ์ดํฐ์์ ํ๋ก์ ํธ ํ์ผ ์ ํ
PROJECT ์น์ ์์ ํ๋ก์ ํธ ์ ํ
Info ํญ์์ Localizations ์น์ ์ฐพ๊ธฐ
+ ๋ฒํผ ํด๋ฆญํด์ ์ํ๋ ์ธ์ด ์ถ๊ฐ (์: Korean, Japanese, Spanish ๋ฑ)
Localizable.strings ํ์ผ ์ ํ
์ค๋ฅธ์ชฝ ์ธ์คํํฐ์์ "Localize..." ๋ฒํผ ํด๋ฆญ
Base ์ธ์ด ์ ํ ํ Localize ํด๋ฆญ
์ธ์คํํฐ์์ ์ถ๊ฐํ ๋ชจ๋ ์ธ์ด ์ฒดํฌ๋ฐ์ค ์ ํ
์ด ๊ณผ์ ์ ๊ฑฐ์น๋ฉด ํ๋ก์ ํธ์ ๊ฐ ์ธ์ด๋ณ๋ก Localizable.strings ํ์ผ์ด ์์ฑ๋ผ! ์๋ฅผ ๋ค์ด: ๐
- Localizable.strings (Base/English)
- Localizable.strings (Korean)
- Localizable.strings (Japanese)
- Localizable.strings (Spanish)
โ๏ธ ๋ฒ์ญ ๋ฌธ์์ด ์์ฑํ๊ธฐ
์ด์ ๊ฐ ํ์ผ์ ์ค์ ๋ฒ์ญ์ ์์ฑํด์ผ ํด. ํ์์ ์์ฃผ ๊ฐ๋จํด! ๐ฏ
/* ์ธ์ฌ๋ง */
"greeting" = "Hello";
/* ํ์ ๋ฉ์์ง */
"welcome_message" = "Welcome to our app!";
/* ๋ก๊ทธ์ธ ๋ฒํผ */
"login_button" = "Login";
/* ์ค์ ํ๋ฉด ์ ๋ชฉ */
"settings_title" = "Settings";
/* ์๋ฆผ ๋ฉ์์ง */
"notification_new_message" = "You have a new message";
/* ์๋ฌ ๋ฉ์์ง */
"error_network" = "Network connection failed. Please try again.";
/* ์ธ์ฌ๋ง */
"greeting" = "์๋
ํ์ธ์";
/* ํ์ ๋ฉ์์ง */
"welcome_message" = "์ฐ๋ฆฌ ์ฑ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค!";
/* ๋ก๊ทธ์ธ ๋ฒํผ */
"login_button" = "๋ก๊ทธ์ธ";
/* ์ค์ ํ๋ฉด ์ ๋ชฉ */
"settings_title" = "์ค์ ";
/* ์๋ฆผ ๋ฉ์์ง */
"notification_new_message" = "์๋ก์ด ๋ฉ์์ง๊ฐ ์์ต๋๋ค";
/* ์๋ฌ ๋ฉ์์ง */
"error_network" = "๋คํธ์ํฌ ์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.";
/* ์ธ์ฌ๋ง */
"greeting" = "ใใใซใกใฏ";
/* ํ์ ๋ฉ์์ง */
"welcome_message" = "็งใใกใฎใขใใชใธใใใใ!";
/* ๋ก๊ทธ์ธ ๋ฒํผ */
"login_button" = "ใญใฐใคใณ";
/* ์ค์ ํ๋ฉด ์ ๋ชฉ */
"settings_title" = "่จญๅฎ";
/* ์๋ฆผ ๋ฉ์์ง */
"notification_new_message" = "ๆฐใใใกใใปใผใธใใใใพใ";
/* ์๋ฌ ๋ฉ์์ง */
"error_network" = "ใใใใฏใผใฏๆฅ็ถใซๅคฑๆใใพใใใใใไธๅบฆใ่ฉฆใใใ ใใใ";
2. ํค ๊ฐ์ ๋ชจ๋ ์ธ์ด ํ์ผ์์ ๋์ผํด์ผ ํด
3. ๋ฐ์ดํ๋ ๋ฐ๋์ ํฐ๋ฐ์ดํ(")๋ฅผ ์ฌ์ฉํด
4. ์ฃผ์์ /* */ ํํ๋ก ์์ฑํ๋ฉฐ, ๋ฒ์ญ๊ฐ๋ค์๊ฒ ํฐ ๋์์ด ๋ผ
5. ํ์ผ ์ธ์ฝ๋ฉ์ UTF-8๋ก ์ค์ ํด์ผ ํด (ํน์๋ฌธ์ ๊นจ์ง ๋ฐฉ์ง)
๐จ ์ค์ ์์ : ๋ค๊ตญ์ด ์ฑ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ๋ก ๋์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ ํ๋กํ ํ๋ฉด์ ๋ค๊ตญ์ด๋ก ๊ตฌํํด๋ณผ๊ฒ. ๐
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค์
import UIKit
class ProfileViewController: UIViewController {
// MARK: - UI Components
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
return label
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
return label
}()
private let bioLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.numberOfLines = 0
return label
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
return button
}()
private let logoutButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
return button
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureLocalizedStrings()
}
// MARK: - Setup
private func setupUI() {
view.backgroundColor = .systemBackground
[titleLabel, nameLabel, emailLabel, bioLabel,
editButton, logoutButton].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40),
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20),
emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
bioLabel.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 20),
bioLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
bioLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.topAnchor.constraint(equalTo: bioLabel.bottomAnchor, constant: 40),
editButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50),
logoutButton.topAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 15),
logoutButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
logoutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
logoutButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func configureLocalizedStrings() {
// ํ๋ฉด ์ ๋ชฉ
titleLabel.text = NSLocalizedString("profile_title",
comment: "ํ๋กํ ํ๋ฉด ์ ๋ชฉ")
// ์ฌ์ฉ์ ์ ๋ณด ๋ ์ด๋ธ
let nameFormat = NSLocalizedString("profile_name_label",
comment: "์ด๋ฆ ๋ ์ด๋ธ")
nameLabel.text = String(format: nameFormat, "๊น์ฒ ์")
let emailFormat = NSLocalizedString("profile_email_label",
comment: "์ด๋ฉ์ผ ๋ ์ด๋ธ")
emailLabel.text = String(format: emailFormat, "chulsoo@example.com")
// ์๊ธฐ์๊ฐ
bioLabel.text = NSLocalizedString("profile_bio_placeholder",
comment: "์๊ธฐ์๊ฐ ๊ธฐ๋ณธ ํ
์คํธ")
// ๋ฒํผ ํ
์คํธ
editButton.setTitle(NSLocalizedString("profile_edit_button",
comment: "ํ๋กํ ์์ ๋ฒํผ"),
for: .normal)
logoutButton.setTitle(NSLocalizedString("profile_logout_button",
comment: "๋ก๊ทธ์์ ๋ฒํผ"),
for: .normal)
}
}
๐ ๋ฒ์ญ ํ์ผ ์์ฑ
/* ํ๋กํ ํ๋ฉด */
"profile_title" = "My Profile";
"profile_name_label" = "Name: %@";
"profile_email_label" = "Email: %@";
"profile_bio_placeholder" = "Tell us about yourself...";
"profile_edit_button" = "Edit Profile";
"profile_logout_button" = "Logout";
/* ์๋ฆผ ๋ฉ์์ง */
"profile_update_success" = "Profile updated successfully!";
"profile_update_failed" = "Failed to update profile. Please try again.";
"logout_confirm_title" = "Logout";
"logout_confirm_message" = "Are you sure you want to logout?";
"confirm_yes" = "Yes";
"confirm_no" = "No";
/* ํ๋กํ ํ๋ฉด */
"profile_title" = "๋ด ํ๋กํ";
"profile_name_label" = "์ด๋ฆ: %@";
"profile_email_label" = "์ด๋ฉ์ผ: %@";
"profile_bio_placeholder" = "์์ ์ ๋ํด ์๊ฐํด์ฃผ์ธ์...";
"profile_edit_button" = "ํ๋กํ ์์ ";
"profile_logout_button" = "๋ก๊ทธ์์";
/* ์๋ฆผ ๋ฉ์์ง */
"profile_update_success" = "ํ๋กํ์ด ์ฑ๊ณต์ ์ผ๋ก ์
๋ฐ์ดํธ๋์์ต๋๋ค!";
"profile_update_failed" = "ํ๋กํ ์
๋ฐ์ดํธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.";
"logout_confirm_title" = "๋ก๊ทธ์์";
"logout_confirm_message" = "์ ๋ง ๋ก๊ทธ์์ํ์๊ฒ ์ต๋๊น?";
"confirm_yes" = "์";
"confirm_no" = "์๋์ค";
/* ํ๋กํ ํ๋ฉด */
"profile_title" = "ใใคใใญใใฃใผใซ";
"profile_name_label" = "ๅๅ: %@";
"profile_email_label" = "ใกใผใซ: %@";
"profile_bio_placeholder" = "่ชๅทฑ็ดนไปใๅ
ฅๅใใฆใใ ใใ...";
"profile_edit_button" = "ใใญใใฃใผใซ็ทจ้";
"profile_logout_button" = "ใญใฐใขใฆใ";
/* ์๋ฆผ ๋ฉ์์ง */
"profile_update_success" = "ใใญใใฃใผใซใๆญฃๅธธใซๆดๆฐใใใพใใ!";
"profile_update_failed" = "ใใญใใฃใผใซใฎๆดๆฐใซๅคฑๆใใพใใใใใไธๅบฆใ่ฉฆใใใ ใใใ";
"logout_confirm_title" = "ใญใฐใขใฆใ";
"logout_confirm_message" = "ๆฌๅฝใซใญใฐใขใฆใใใพใใ?";
"confirm_yes" = "ใฏใ";
"confirm_no" = "ใใใ";
์ฌ๊ธฐ์ ์ฃผ๋ชฉํ ์ ์ %@ ๊ฐ์ ํฌ๋งท ์ง์ ์๋ฅผ ์ฌ์ฉํ๋ค๋ ๊ฑฐ์ผ! ์ด๊ฑด ๋์ ์ผ๋ก ๊ฐ์ ๋ฃ์ ์ ์๊ฒ ํด์ฃผ๋ ํ๋ ์ด์คํ๋์ผ. String(format:) ํจ์์ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ ๋ง ์ ์ฉํ์ง! ๐ฏ
๐ข ๋ณต์ํ๊ณผ ๋ณ์ ์ฒ๋ฆฌํ๊ธฐ
๋ค๊ตญ์ด ์ง์์์ ๊น๋ค๋ก์ด ๋ถ๋ถ ์ค ํ๋๊ฐ ๋ฐ๋ก ๋ณต์ํ ์ฒ๋ฆฌ์ผ. ์ธ์ด๋ง๋ค ๋ณต์ํ ๊ท์น์ด ์์ ํ ๋ค๋ฅด๊ฑฐ๋ ! ๐ต
์๋ฅผ ๋ค์ด๋ณผ๊ฒ:
- ์์ด: "1 item", "2 items" (๋จ์/๋ณต์ 2๊ฐ์ง)
- ํ๊ตญ์ด: "1๊ฐ", "2๊ฐ" (๋ณต์ํ ๊ตฌ๋ถ ์์)
- ๋ฌ์์์ด: 1, 21, 31... / 2-4, 22-24... / 5-20, 25-30... (3๊ฐ์ง ํํ!)
- ์๋์ด: 0๊ฐ, 1๊ฐ, 2๊ฐ, 3-10๊ฐ, 11-99๊ฐ, 100๊ฐ ์ด์ (6๊ฐ์ง ํํ!!!) ๐ฑ
๐ Stringsdict ํ์ผ ํ์ฉํ๊ธฐ
iOS๋ ์ด๋ฐ ๋ณต์กํ ๋ณต์ํ ์ฒ๋ฆฌ๋ฅผ ์ํด .stringsdict ํ์ผ์ ์ ๊ณตํด. ์ด๊ฑด XML ํ์์ ํ์ผ์ธ๋ฐ, ๋ณต์ํ ๊ท์น์ ์ ์ํ ์ ์์ด! ๐
File โ New โ File... ์ ํ
Resource ์น์ ์์ "Stringsdict File" ์ ํ
ํ์ผ ์ด๋ฆ์ "Localizable"๋ก ์ ๋ ฅ
Localizable.strings์ ๋์ผํ๊ฒ ์ง์ญํ ์ค์
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@item_count@</string>
<key>item_count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>1 item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
<key>messages_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@message_count@</string>
<key>message_count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No new messages</string>
<key>one</key>
<string>1 new message</string>
<key>other</key>
<string>%d new messages</string>
</dict>
</dict>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@item_count@</string>
<key>item_count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%d๊ฐ์ ํญ๋ชฉ</string>
</dict>
</dict>
<key>messages_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@message_count@</string>
<key>message_count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>์ ๋ฉ์์ง %d๊ฐ</string>
</dict>
</dict>
</dict>
</plist>
๐ป ์ฝ๋์์ ์ฌ์ฉํ๊ธฐ
class ShoppingCartViewController: UIViewController {
var itemCount: Int = 0 {
didSet {
updateItemCountLabel()
}
}
private let itemCountLabel = UILabel()
private func updateItemCountLabel() {
// stringsdict ํ์ผ์ ๋ณต์ํ ๊ท์น์ด ์๋์ผ๋ก ์ ์ฉ๋จ!
let format = NSLocalizedString("items_count", comment: "์ฅ๋ฐ๊ตฌ๋ ์์ดํ
๊ฐ์")
itemCountLabel.text = String.localizedStringWithFormat(format, itemCount)
// itemCount๊ฐ 0์ด๋ฉด: "No items" (์์ด) / "0๊ฐ์ ํญ๋ชฉ" (ํ๊ตญ์ด)
// itemCount๊ฐ 1์ด๋ฉด: "1 item" (์์ด) / "1๊ฐ์ ํญ๋ชฉ" (ํ๊ตญ์ด)
// itemCount๊ฐ 5์ด๋ฉด: "5 items" (์์ด) / "5๊ฐ์ ํญ๋ชฉ" (ํ๊ตญ์ด)
}
private func showNewMessagesAlert(count: Int) {
let format = NSLocalizedString("messages_count", comment: "์ ๋ฉ์์ง ๊ฐ์")
let message = String.localizedStringWithFormat(format, count)
let alert = UIAlertController(title: NSLocalizedString("notification_title",
comment: "์๋ฆผ ์ ๋ชฉ"),
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("confirm_ok",
comment: "ํ์ธ ๋ฒํผ"),
style: .default))
present(alert, animated: true)
}
}
2. ์์ด๋ "zero", "one", "other" ์ธ ๊ฐ์ง๋ง ์์ผ๋ฉด ์ถฉ๋ถํด
3. ๋ณต์กํ ์ธ์ด(๋ฌ์์์ด, ์๋์ด ๋ฑ)๋ฅผ ์ง์ํ ๋๋ Apple์ ๊ณต์ ๋ฌธ์๋ฅผ ๊ผญ ์ฐธ๊ณ ํด!
4. String.localizedStringWithFormat๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฐ๋์ NSLocalizedString์ผ๋ก ํฌ๋งท์ ๋จผ์ ๊ฐ์ ธ์์ผ ํด
๐ญ ์ฌ๋ฌ ๋ณ์๊ฐ ์๋ ๊ฒฝ์ฐ
๋๋ก๋ ํ๋์ ๋ฌธ์์ด์ ์ฌ๋ฌ ๊ฐ์ ๋ณ์๋ฅผ ๋ฃ์ด์ผ ํ ๋๊ฐ ์์ด. ์๋ฅผ ๋ค์ด "๊น์ฒ ์๋์ด 5๊ฐ์ ๋ฉ์์ง๋ฅผ ๋ณด๋์ต๋๋ค" ๊ฐ์ ๊ฒฝ์ฐ ๋ง์ด์ผ! ๐ค
// Localizable.strings (English)
"message_from_user" = "%@ sent you %d messages";
// Localizable.strings (Korean)
"message_from_user" = "%@๋์ด ๋ฉ์์ง %d๊ฐ๋ฅผ ๋ณด๋์ต๋๋ค";
// Swift ์ฝ๋
let userName = "๊น์ฒ ์"
let messageCount = 5
let format = NSLocalizedString("message_from_user",
comment: "์ฌ์ฉ์๋ก๋ถํฐ ๋ฐ์ ๋ฉ์์ง")
let message = String(format: format, userName, messageCount)
// ๊ฒฐ๊ณผ:
// ์์ด: "๊น์ฒ ์ sent you 5 messages"
// ํ๊ตญ์ด: "๊น์ฒ ์๋์ด ๋ฉ์์ง 5๊ฐ๋ฅผ ๋ณด๋์ต๋๋ค"
์ฌ๊ธฐ์ ์ค์ํ ๊ฑด ๋ณ์์ ์์์ผ! ์ธ์ด๋ง๋ค ๋ฌธ์ฅ ๊ตฌ์กฐ๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์, ๋ณ์์ ์์น๋ ๋ฌ๋ผ์ง ์ ์์ด. ๊ทธ๋์ ํฌ๋งท ์ง์ ์์ ์์๋ฅผ ๋ช ์ํ ์๋ ์์ด! ๐ข
// ๋ณ์ ์์๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์
// Localizable.strings (English)
"user_action" = "%1$@ deleted %2$d items from %3$@";
// Localizable.strings (Korean)
"user_action" = "%1$@๋์ด %3$@์์ %2$d๊ฐ์ ํญ๋ชฉ์ ์ญ์ ํ์ต๋๋ค";
// Swift ์ฝ๋
let format = NSLocalizedString("user_action", comment: "์ฌ์ฉ์ ์ก์
")
let message = String(format: format, "๊น์ฒ ์", 3, "์ฅ๋ฐ๊ตฌ๋")
// ์์ด: "๊น์ฒ ์ deleted 3 items from ์ฅ๋ฐ๊ตฌ๋"
// ํ๊ตญ์ด: "๊น์ฒ ์๋์ด ์ฅ๋ฐ๊ตฌ๋์์ 3๊ฐ์ ํญ๋ชฉ์ ์ญ์ ํ์ต๋๋ค"
%1$@, %2$d, %3$@ ์ด๋ฐ ์์ผ๋ก ์ซ์๋ฅผ ๋ถ์ด๋ฉด ์์๋ฅผ ๋ช ์ํ ์ ์์ด! ์ด๋ ๊ฒ ํ๋ฉด ์ธ์ด๋ณ๋ก ์์ ๋กญ๊ฒ ๋ณ์ ์์๋ฅผ ๋ฐ๊ฟ ์ ์์ง. ์ ๋ง ์ ์ฉํ์ง? ๐
๐จ ์ด๋ฏธ์ง์ ์์ ์ง์ญํ
ํ
์คํธ๋ง ๋ฒ์ญํ๋ค๊ณ ๋์ด ์๋์ผ! ์ด๋ฏธ์ง๋ ์์ด์ฝ์๋ ํ
์คํธ๊ฐ ๋ค์ด๊ฐ ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์? ๐ผ
์๋ฅผ ๋ค์ด "Welcome" ํ
์คํธ๊ฐ ๋ค์ด๊ฐ ๋ฐฐ๋ ์ด๋ฏธ์ง๋, ํน์ ๊ตญ๊ฐ์ ๋ฌธํ๋ฅผ ๋ฐ์ํ ์์ด์ฝ ๊ฐ์ ๊ฑฐ ๋ง์ด์ผ. ์ด๋ฐ ๊ฒ๋ค๋ ์ง์ญํ๊ฐ ํ์ํด! ๐
๐ฆ Asset Catalog์์ ์ด๋ฏธ์ง ์ง์ญํํ๊ธฐ
Assets.xcassets ํ์ผ ์ด๊ธฐ
์ง์ญํํ ์ด๋ฏธ์ง ์ ํ
์ค๋ฅธ์ชฝ ์ธ์คํํฐ์์ "Localize..." ๋ฒํผ ํด๋ฆญ
์ง์ํ ์ธ์ด๋ค ์ ํ
๊ฐ ์ธ์ด๋ณ๋ก ์ ์ ํ ์ด๋ฏธ์ง๋ฅผ ๋๋๊ทธ ์ค ๋๋กญ
์ด๋ฏธ์ง ์ด๋ฆ์ ๋์ผํ๊ฒ ์ ์ง
์์คํ ์ด ์๋์ผ๋ก ์ธ์ด์ ๋ง๋ ์ด๋ฏธ์ง๋ฅผ ์ ํํจ
class WelcomeViewController: UIViewController {
private let bannerImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
// ์์คํ
์ด ์๋์ผ๋ก ํ์ฌ ์ธ์ด์ ๋ง๋ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํจ
imageView.image = UIImage(named: "welcome_banner")
return imageView
}()
private let iconImageView: UIImageView = {
let imageView = UIImageView()
// ๊ตญ๊ฐ๋ณ๋ก ๋ค๋ฅธ ์์ด์ฝ ํ์
imageView.image = UIImage(named: "country_icon")
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
// ํ์ฌ ์ธ์ด ํ์ธ (๋๋ฒ๊น
์ฉ)
let currentLanguage = Locale.current.languageCode
print("Current language: \(currentLanguage ?? "unknown")")
}
}
๐ฏ SF Symbols ํ์ฉํ๊ธฐ
iOS 13๋ถํฐ ์ ๊ณต๋๋ SF Symbols๋ ์๋์ผ๋ก RTL(Right-to-Left) ์ธ์ด๋ฅผ ์ง์ํด! ์๋์ด๋ ํ๋ธ๋ฆฌ์ด ๊ฐ์ ์ธ์ด์์ ์์ด์ฝ์ด ์๋์ผ๋ก ๋ค์งํ์ ํ์๋๋ ๊ฑฐ์ง. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
class NavigationViewController: UIViewController {
private func setupNavigationButtons() {
// ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ - RTL ์ธ์ด์์ ์๋์ผ๋ก ๋ฐฉํฅ์ด ๋ฐ๋
let backButton = UIButton()
let backImage = UIImage(systemName: "chevron.left")?
.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20))
backButton.setImage(backImage, for: .normal)
// ์์ผ๋ก๊ฐ๊ธฐ ๋ฒํผ
let forwardButton = UIButton()
let forwardImage = UIImage(systemName: "chevron.right")?
.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20))
forwardButton.setImage(forwardImage, for: .normal)
// RTL ์ธ์ด์์๋ ์๋์ผ๋ก ์ข์ฐ๊ฐ ๋ฐ๋!
// ์๋์ด: chevron.left๊ฐ ์ค๋ฅธ์ชฝ์ ๊ฐ๋ฆฌํด
// ์์ด/ํ๊ตญ์ด: chevron.left๊ฐ ์ผ์ชฝ์ ๊ฐ๋ฆฌํด
}
// ํน์ ์ฌ๋ณผ์ ๋ฐฉํฅ์ ๊ฐ์ ๋ก ์ง์ ํ๊ณ ์ถ๋ค๋ฉด
private func createDirectionalSymbol() {
let config = UIImage.SymbolConfiguration(pointSize: 20)
// ํญ์ ์ผ์ชฝ์ ๊ฐ๋ฆฌํค๋ ํ์ดํ (RTL์์๋ ๋ณํ์ง ์์)
let leftArrow = UIImage(systemName: "arrow.left",
withConfiguration: config)?
.withHorizontallyFlippedOrientation()
// ์ธ์ด์ ๋ฐ๋ผ ์๋์ผ๋ก ๋ฐฉํฅ์ด ๋ฐ๋๋ ํ์ดํ
let autoArrow = UIImage(systemName: "arrow.forward",
withConfiguration: config)
}
}
๐ RTL(Right-to-Left) ์ธ์ด ์ง์
์๋์ด, ํ๋ธ๋ฆฌ์ด, ํ๋ฅด์์์ด ๊ฐ์ ์ธ์ด๋ค์ ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ๊ธ์ ์จ. ์ด๋ฐ ์ธ์ด๋ค์ ์ง์ํ๋ ค๋ฉด UI ๋ ์ด์์๋ ์์ ํ ๋ค์ง์ด์ผ ํด! ๐ฎ
๋คํํ iOS๋ ์ด๋ฐ RTL ์ง์์ ์ ๋ง ์ ํด์ค. Auto Layout์ ์ ๋๋ก ์ฌ์ฉํ๋ค๋ฉด ๋๋ถ๋ถ ์๋์ผ๋ก ์ฒ๋ฆฌ๋ผ! ๐
๐ Auto Layout์ผ๋ก RTL ๋์ํ๊ธฐ
- leadingAnchor / trailingAnchor ์ฌ์ฉ
- UIStackView ํ์ฉ
- semanticContentAttribute ์ค์
๋์ ์:
- leftAnchor / rightAnchor ์ง์ ์ฌ์ฉ
- ์ ๋ ์ขํ ์ฌ์ฉ
- ํ๋์ฝ๋ฉ๋ ๋ฐฉํฅ์ฑ
class MessageCell: UITableViewCell {
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 25
imageView.clipsToBounds = true
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .semibold)
return label
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.numberOfLines = 2
return label
}()
private let timeLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .gray
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupLayout() {
[profileImageView, nameLabel, messageLabel, timeLabel].forEach {
contentView.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
// โ
leading/trailing ์ฌ์ฉ - RTL์์ ์๋์ผ๋ก ๋ค์งํ
profileImageView.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor,
constant: 15
),
profileImageView.centerYAnchor.constraint(
equalTo: contentView.centerYAnchor
),
profileImageView.widthAnchor.constraint(equalToConstant: 50),
profileImageView.heightAnchor.constraint(equalToConstant: 50),
// ํ๋กํ ์ด๋ฏธ์ง ๋ค์์ ์์น (RTL์์๋ ์ค๋ฅธ์ชฝ์ ์์น)
nameLabel.leadingAnchor.constraint(
equalTo: profileImageView.trailingAnchor,
constant: 12
),
nameLabel.topAnchor.constraint(
equalTo: contentView.topAnchor,
constant: 12
),
nameLabel.trailingAnchor.constraint(
equalTo: timeLabel.leadingAnchor,
constant: -8
),
messageLabel.leadingAnchor.constraint(
equalTo: nameLabel.leadingAnchor
),
messageLabel.topAnchor.constraint(
equalTo: nameLabel.bottomAnchor,
constant: 4
),
messageLabel.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor,
constant: -15
),
// ์๊ฐ์ ํญ์ ๋์ชฝ์ ์์น (RTL์์๋ ์ผ์ชฝ)
timeLabel.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor,
constant: -15
),
timeLabel.centerYAnchor.constraint(
equalTo: nameLabel.centerYAnchor
)
])
}
}
๐จ ํ ์คํธ ์ ๋ ฌ ์ฒ๋ฆฌํ๊ธฐ
class LocalizedTextView: UIView {
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
// โ
natural ์ ๋ ฌ - ์ธ์ด์ ๋ฐ๋ผ ์๋์ผ๋ก ์กฐ์ ๋จ
label.textAlignment = .natural
return label
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
// LTR ์ธ์ด: ์ผ์ชฝ ์ ๋ ฌ
// RTL ์ธ์ด: ์ค๋ฅธ์ชฝ ์ ๋ ฌ
label.textAlignment = .natural
return label
}()
private let centeredLabel: UILabel = {
let label = UILabel()
// ์ค์ ์ ๋ ฌ์ ๋ชจ๋ ์ธ์ด์์ ๋์ผ
label.textAlignment = .center
return label
}()
// ํน์ ๊ฒฝ์ฐ์ ๊ฐ์ ๋ก ๋ฐฉํฅ ์ง์ ์ด ํ์ํ ๋
private func forceLeftAlignment() {
let label = UILabel()
// โ ์ด๋ ๊ฒ ํ๋ฉด RTL ์ธ์ด์์๋ ์ผ์ชฝ ์ ๋ ฌ๋จ
label.textAlignment = .left
// โ
๋์ ์ด๋ ๊ฒ ํ๋ ๊ฒ ์ข์
if UIView.userInterfaceLayoutDirection(
for: label.semanticContentAttribute
) == .rightToLeft {
label.textAlignment = .right
} else {
label.textAlignment = .left
}
}
}
๐ง UIStackView๋ก ๊ฐํธํ๊ฒ RTL ๋์ํ๊ธฐ
UIStackView๋ฅผ ์ฌ์ฉํ๋ฉด RTL ๋์์ด ์ ๋ง ์ฌ์์ ธ! ์๋์ผ๋ก ์์๊ฐ ๋ฐ๋๊ฑฐ๋ . ๐
class ProfileHeaderView: UIView {
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 12
stack.alignment = .center
// โ
์ด ํ ์ค๋ก RTL ์๋ ๋์!
stack.semanticContentAttribute = .unspecified
return stack
}()
private let profileImageView = UIImageView()
private let nameLabel = UILabel()
private let badgeImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
// LTR: [์ด๋ฏธ์ง] [์ด๋ฆ] [๋ฑ์ง]
// RTL: [๋ฑ์ง] [์ด๋ฆ] [์ด๋ฏธ์ง]
// ์๋์ผ๋ก ์์๊ฐ ๋ฐ๋!
stackView.addArrangedSubview(profileImageView)
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(badgeImageView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
])
}
}
2. ์๋ฎฌ๋ ์ดํฐ ์ค์ ์์ ์ธ์ด๋ฅผ ๋ฐ๊ฟ๊ฐ๋ฉฐ ํ์ธ
3. Debug View Hierarchy๋ก ๋ ์ด์์ ํ์ธ
4. ์ค์ RTL ์ธ์ด ์ฌ์ฉ์์๊ฒ ํผ๋๋ฐฑ ๋ฐ๊ธฐ (๊ฐ๋ฅํ๋ค๋ฉด!)
๐ ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ด์ ์ค์ ์์ ์ ๋ง ์ ์ฉํ ํ๋ค์ ๊ณต์ ํด๋ณผ๊ฒ! ์ด๊ฑด ๋ด๊ฐ ์ค์ ํ๋ก์ ํธ์์ ๊ฒช์ผ๋ฉด์ ๋ฐฐ์ด ๊ฒ๋ค์ด์ผ. ๐
๐ ๋ฌธ์์ด ํค ๊ด๋ฆฌ ์ ๋ต
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด ๋ฌธ์์ด ํค๊ฐ ์๋ฐฑ ๊ฐ๊ฐ ๋ ์ ์์ด. ์ด๊ฑธ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒ ์ ๋ง ์ค์ํด! ๐
// Localizable.swift
enum Localizable {
// MARK: - Common
enum Common {
static let ok = "common_ok".localized
static let cancel = "common_cancel".localized
static let confirm = "common_confirm".localized
static let delete = "common_delete".localized
static let save = "common_save".localized
static let edit = "common_edit".localized
}
// MARK: - Authentication
enum Auth {
static let loginTitle = "auth_login_title".localized
static let loginButton = "auth_login_button".localized
static let signupButton = "auth_signup_button".localized
static let forgotPassword = "auth_forgot_password".localized
static func welcomeMessage(name: String) -> String {
String(format: "auth_welcome_message".localized, name)
}
}
// MARK: - Profile
enum Profile {
static let title = "profile_title".localized
static let editButton = "profile_edit_button".localized
static let logoutButton = "profile_logout_button".localized
static func itemsCount(_ count: Int) -> String {
String.localizedStringWithFormat(
"profile_items_count".localized,
count
)
}
}
// MARK: - Errors
enum Error {
static let networkError = "error_network".localized
static let unknownError = "error_unknown".localized
static let invalidInput = "error_invalid_input".localized
static func customError(message: String) -> String {
String(format: "error_custom".localized, message)
}
}
}
// String Extension
extension String {
var localized: String {
return NSLocalizedString(self, comment: "")
}
func localized(with arguments: CVarArg...) -> String {
return String(format: self.localized, arguments: arguments)
}
}
// ์ฌ์ฉ ์์
class LoginViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// โ
์ด๋ ๊ฒ ๊น๋ํ๊ฒ ์ฌ์ฉ!
titleLabel.text = Localizable.Auth.loginTitle
loginButton.setTitle(Localizable.Auth.loginButton, for: .normal)
// ๋์ ๋ฉ์์ง
let userName = "๊น์ฒ ์"
welcomeLabel.text = Localizable.Auth.welcomeMessage(name: userName)
}
private func showError() {
let alert = UIAlertController(
title: Localizable.Error.networkError,
message: Localizable.Error.customError(message: "์ฐ๊ฒฐ ์คํจ"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(
title: Localizable.Common.ok,
style: .default
))
present(alert, animated: true)
}
}
์ด๋ ๊ฒ enum์ผ๋ก ๊ตฌ์กฐํํ๋ฉด ์ฌ๋ฌ ๊ฐ์ง ์ฅ์ ์ด ์์ด: ๐ฏ
1. ์๋์์ฑ: Xcode๊ฐ ์๋์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฌธ์์ด์ ๋ณด์ฌ์ค
2. ํ์
์์ ์ฑ: ์คํ๋ก ์ธํ ๋ฒ๊ทธ๋ฅผ ์ปดํ์ผ ํ์์ ์ก์ ์ ์์ด
3. ๊ฐ๋
์ฑ: ์ฝ๋๋ง ๋ด๋ ์ด๋ค ๋ฌธ์์ด์ธ์ง ๋ช
ํํด
4. ์ ์ง๋ณด์: ๋ฌธ์์ด ํค๋ฅผ ๋ฐ๊ฟ๋ ํ ๊ณณ๋ง ์์ ํ๋ฉด ๋ผ
๐ ๋ฒ์ญ ๋๋ฝ ์ฐพ๊ธฐ
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด ๋ฒ์ญ์ ๊น๋นกํ๋ ๊ฒฝ์ฐ๊ฐ ์๊ฒจ. ์ด๊ฑธ ์๋์ผ๋ก ์ฒดํฌํ๋ ๋ฐฉ๋ฒ์ด ์์ด! ๐ต๏ธ
#!/bin/bash
# check_localization.sh
# ๋ชจ๋ .strings ํ์ผ์ ํค๋ฅผ ๋น๊ตํด์ ๋๋ฝ๋ ๋ฒ์ญ์ ์ฐพ์
BASE_FILE="en.lproj/Localizable.strings"
LANG_FILES=("ko.lproj/Localizable.strings" "ja.lproj/Localizable.strings")
echo "๐ ๋ฒ์ญ ๋๋ฝ ์ฒดํฌ ์์..."
# Base ํ์ผ์ ๋ชจ๋ ํค ์ถ์ถ
base_keys=$(grep -o '"[^"]*"' "$BASE_FILE" | grep -o '[^"]*' | sort | uniq)
for lang_file in "${LANG_FILES[@]}"; do
echo "\n๐ ์ฒดํฌ ์ค: $lang_file"
# ํด๋น ์ธ์ด ํ์ผ์ ํค ์ถ์ถ
lang_keys=$(grep -o '"[^"]*"' "$lang_file" | grep -o '[^"]*' | sort | uniq)
# ๋๋ฝ๋ ํค ์ฐพ๊ธฐ
missing_keys=$(comm -23 <(echo "$base_keys") <(echo "$lang_keys"))
if [ -z "$missing_keys" ]; then
echo "โ
๋ชจ๋ ๋ฒ์ญ์ด ์๋ฃ๋์์ต๋๋ค!"
else
echo "โ ๋๋ฝ๋ ๋ฒ์ญ:"
echo "$missing_keys"
fi
done
echo "\nโจ ์ฒดํฌ ์๋ฃ!"
์ด ์คํฌ๋ฆฝํธ๋ฅผ Xcode์ Build Phase์ ์ถ๊ฐํ๋ฉด, ๋น๋ํ ๋๋ง๋ค ์๋์ผ๋ก ์ฒดํฌํ ์ ์์ด! ์ ๋ง ์ ์ฉํ์ง? ๐
๐ฏ ๋ฒ์ญ ํ์ง ๊ด๋ฆฌ
- โ ๋ฌธ๋งฅ์ ๊ณ ๋ คํ ๋ฒ์ญ (comment ํ์ฉ)
- โ ์ผ๊ด๋ ์ฉ์ด ์ฌ์ฉ (์ฉ์ด์ง ๋ง๋ค๊ธฐ)
- โ ์์ฐ์ค๋ฌ์ด ํํ (์ง์ญ X)
- โ ๋ฌธํ์ ์ฐจ์ด ๊ณ ๋ ค
- โ ๊ธธ์ด ์ ํ ํ์ธ (UI์ ๋ง๊ฒ)
- โ ์ ๋ฌธ ๋ฒ์ญ๊ฐ ๊ฒํ (๊ฐ๋ฅํ๋ค๋ฉด)
- โ ๋ค์ดํฐ๋ธ ์คํผ์ปค ํ ์คํธ
// TranslationGuidelines.md
# ๋ฒ์ญ ๊ฐ์ด๋๋ผ์ธ
## ์ฉ์ด ํต์ผ
- User โ ์ฌ์ฉ์ (์ ์ X)
- Settings โ ์ค์
- Profile โ ํ๋กํ
- Message โ ๋ฉ์์ง
## ํค ์ค ๋งค๋
- ์กด๋๋ง ์ฌ์ฉ (~ํฉ๋๋ค, ~ํ์ธ์)
- ์น๊ทผํ๊ณ ๋ฐ๋ปํ ๋๋
- ๊ธฐ์ ์ฉ์ด๋ ํ๊ธํ๋ ํํ ์ฐ์
## ์ฃผ์์ฌํญ
- ๋ฒํผ ํ
์คํธ๋ 2-3๋จ์ด ์ด๋ด
- ์๋ฌ ๋ฉ์์ง๋ ๊ตฌ์ฒด์ ์ผ๋ก
- ๊ธ์ ์ ์ธ ํํ ์ฌ์ฉ
## ๋ฌธํ์ ๊ณ ๋ ค์ฌํญ
- ์์ ์๋ฏธ (๋นจ๊ฐ = ์ํ/ํ์ด)
- ์ซ์ ์๋ฏธ (4 = ๋ถ๊ธธ)
- ์ ์ค์ฒ ์ฐจ์ด
๐ ์ฑ๋ฅ ์ต์ ํ
NSLocalizedString์ ๋งค๋ฒ ํ์ผ์ ์ฝ์ด์ค๋ ๊ฒ ์๋๋ผ ์บ์ฑ๋๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ ๊ฑฑ์ ์ ์ ํด๋ ๋ผ! ํ์ง๋ง ๋ ์ต์ ํํ๊ณ ์ถ๋ค๋ฉด? ๐ค
class LocalizationManager {
static let shared = LocalizationManager()
private var cache: [String: String] = [:]
private let queue = DispatchQueue(label: "com.app.localization",
attributes: .concurrent)
private init() {
// ์ฑ ์์ ์ ์์ฃผ ์ฌ์ฉํ๋ ๋ฌธ์์ด ๋ฏธ๋ฆฌ ๋ก๋
preloadCommonStrings()
}
private func preloadCommonStrings() {
let commonKeys = [
"common_ok",
"common_cancel",
"common_confirm",
"common_delete",
"common_save"
]
commonKeys.forEach { key in
cache[key] = NSLocalizedString(key, comment: "")
}
}
func localizedString(for key: String, comment: String = "") -> String {
// ์บ์ ํ์ธ
if let cached = queue.sync(execute: { cache[key] }) {
return cached
}
// ์บ์์ ์์ผ๋ฉด ๋ก๋ํ๊ณ ์ ์ฅ
let localized = NSLocalizedString(key, comment: comment)
queue.async(flags: .barrier) { [weak self] in
self?.cache[key] = localized
}
return localized
}
func clearCache() {
queue.async(flags: .barrier) { [weak self] in
self?.cache.removeAll()
}
}
}
// ์ฌ์ฉ ์์
extension String {
var localizedCached: String {
return LocalizationManager.shared.localizedString(for: self)
}
}
ํ์ง๋ง ์์งํ ๋งํ๋ฉด, ๋๋ถ๋ถ์ ์ฑ์์๋ ์ด๋ฐ ์ต์ ํ๊ฐ ํ์ ์์ด. iOS๊ฐ ์ด๋ฏธ ์ถฉ๋ถํ ๋น ๋ฅด๊ฑฐ๋ ! ๐
์ ๋ง ์์ฒ ๊ฐ์ ๋ฌธ์์ด์ ๋์์ ๋ก๋ํ๋ ํน์ํ ๊ฒฝ์ฐ์๋ง ๊ณ ๋ คํด๋ด.
๐ ์์ฃผ ๋ง๋๋ ๋ฌธ์ ์ ํด๊ฒฐ๋ฒ
๊ตญ์ ํ ์์ ์ ํ๋ค ๋ณด๋ฉด ์์์น ๋ชปํ ๋ฌธ์ ๋ค์ ๋ง๋๊ฒ ๋ผ. ๋ด๊ฐ ๊ฒช์๋ ๋ํ์ ์ธ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ๋ฒ์ ๊ณต์ ํ ๊ฒ! ๐ง
โ ๋ฌธ์ 1: ๋ฒ์ญ์ด ์ ์ฉ๋์ง ์์์!
// 1. ํ์ผ ์ด๋ฆ ํ์ธ
// โ ์๋ชป๋ ์ด๋ฆ
"Localized.strings"
"localizable.strings"
"LocalizableStrings.strings"
// โ
์ฌ๋ฐ๋ฅธ ์ด๋ฆ
"Localizable.strings"
// 2. ํ์ผ ์์น ํ์ธ
// Localizable.strings ํ์ผ์ด ์ฌ๋ฐ๋ฅธ .lproj ํด๋ ์์ ์๋์ง ํ์ธ
// ์: ko.lproj/Localizable.strings
// 3. ๋น๋ ํ๊ฒ ํ์ธ
// ํ์ผ ์ธ์คํํฐ์์ Target Membership์ด ์ฒดํฌ๋์ด ์๋์ง ํ์ธ
// 4. ํด๋ฆฐ ๋น๋
// Product โ Clean Build Folder (Cmd + Shift + K)
// ๊ทธ ๋ค์ ๋ค์ ๋น๋
// 5. ์๋ฎฌ๋ ์ดํฐ ์ธ์ด ์ค์ ํ์ธ
// Settings โ General โ Language & Region
// ์ํ๋ ์ธ์ด๊ฐ ์ ํ๋์ด ์๋์ง ํ์ธ
// 6. ๋๋ฒ๊น
์ฝ๋๋ก ํ์ธ
let currentLanguage = Locale.preferredLanguages.first
print("Current language: \(currentLanguage ?? "unknown")")
let testString = NSLocalizedString("test_key", comment: "")
print("Localized string: \(testString)")
โ ๋ฌธ์ 2: ํน์๋ฌธ์๊ฐ ๊นจ์ ธ์!
// 1. ํ์ผ ์ธ์ฝ๋ฉ ํ์ธ
// Xcode์์ Localizable.strings ํ์ผ ์ ํ
// File Inspector โ Text Settings โ Text Encoding
// "UTF-8" ์ ํ
// 2. ์ด๋ฏธ ๊นจ์ง ํ์ผ ๋ณต๊ตฌ
// ํฐ๋ฏธ๋์์ ํ์ผ ์ธ์ฝ๋ฉ ๋ณํ
// iconv -f ISO-8859-1 -t UTF-8 Localizable.strings > Localizable_fixed.strings
// 3. ์๋ก ์์ฑํ ๋ ์ฃผ์
// Xcode์์ ์ง์ ์์ฑํ๋ฉด ์๋์ผ๋ก UTF-8๋ก ์ ์ฅ๋จ
// ์ธ๋ถ ์๋ํฐ ์ฌ์ฉ ์ ๋ฐ๋์ UTF-8๋ก ์ ์ฅ
// 4. ์ด์ค์ผ์ดํ ์ํ์ค ์ฌ์ฉ
"special_chars" = "๋ฐ์ดํ: \" ๋ฐฑ์ฌ๋์: \\ ์ค๋ฐ๊ฟ: \n";
โ ๋ฌธ์ 3: ๋ฌธ์์ด์ด UI์ ๋ค ์ ๋ค์ด๊ฐ์!
class AdaptiveButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupButton()
}
private func setupButton() {
// 1. ์๋ ํฐํธ ํฌ๊ธฐ ์กฐ์
titleLabel?.adjustsFontSizeToFitWidth = true
titleLabel?.minimumScaleFactor = 0.8
// 2. ์ฌ๋ฌ ์ค ํ์ฉ (ํ์ํ ๊ฒฝ์ฐ)
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
// 3. ์ถฉ๋ถํ ํจ๋ฉ ์ค์
contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
}
}
class AdaptiveLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupLabel()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLabel()
}
private func setupLabel() {
// 1. ์๋ ์ค๋ฐ๊ฟ
numberOfLines = 0
lineBreakMode = .byWordWrapping
// 2. ๋์ ํ์
์ง์
adjustsFontForContentSizeCategory = true
// 3. ์ต์ ํฐํธ ํฌ๊ธฐ ์ค์
adjustsFontSizeToFitWidth = true
minimumScaleFactor = 0.7
}
}
// 3. ์ ์ฝ์กฐ๊ฑด ์ฐ์ ์์ ์กฐ์
class FlexibleView: UIView {
private let label = UILabel()
private func setupConstraints() {
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
let widthConstraint = label.widthAnchor.constraint(equalToConstant: 200)
// ์ฐ์ ์์๋ฅผ ๋ฎ์ถฐ์ ํ์ํ๋ฉด ๋์ด๋ ์ ์๊ฒ
widthConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 15),
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -15),
label.centerYAnchor.constraint(equalTo: centerYAnchor),
widthConstraint
])
}
}
โ ๋ฌธ์ 4: ๋ ์ง/์ซ์ ํ์์ด ์ด์ํด์!
class LocalizedFormatters {
// MARK: - Date Formatter
static func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.dateStyle = style
formatter.timeStyle = .none
// ์ฌ์ฉ์์ ๋ก์ผ์ผ์ ๋ง๊ฒ ์๋ ์กฐ์
formatter.locale = Locale.current
return formatter.string(from: date)
}
static func formatDateTime(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = Locale.current
return formatter.string(from: date)
}
// MARK: - Number Formatter
static func formatNumber(_ number: Double, decimals: Int = 2) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = decimals
formatter.maximumFractionDigits = decimals
formatter.locale = Locale.current
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
}
// MARK: - Currency Formatter
static func formatCurrency(_ amount: Double, currencyCode: String = "USD") -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
formatter.locale = Locale.current
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
}
// MARK: - Percentage Formatter
static func formatPercentage(_ value: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
formatter.locale = Locale.current
return formatter.string(from: NSNumber(value: value)) ?? "\(value * 100)%"
}
}
// ์ฌ์ฉ ์์
class StatisticsViewController: UIViewController {
private func updateUI() {
let now = Date()
let price = 1234.56
let percentage = 0.1234
// ๋ ์ง: ํ๊ตญ "2024๋
1์ 15์ผ", ๋ฏธ๊ตญ "January 15, 2024"
dateLabel.text = LocalizedFormatters.formatDate(now)
// ์ซ์: ํ๊ตญ "1,234.56", ๋
์ผ "1.234,56"
numberLabel.text = LocalizedFormatters.formatNumber(price)
// ํตํ: ํ๊ตญ "โฉ1,235", ๋ฏธ๊ตญ "$1,234.56"
priceLabel.text = LocalizedFormatters.formatCurrency(price, currencyCode: "KRW")
// ํผ์ผํธ: "12.34%"
percentLabel.text = LocalizedFormatters.formatPercentage(percentage)
}
}
โ ๋ฌธ์ 5: ์ฑ ๋ด์์ ์ธ์ด๋ฅผ ๋ฐ๊พธ๊ณ ์ถ์ด์!
class LanguageManager {
static let shared = LanguageManager()
private let languageKey = "AppLanguage"
var currentLanguage: String {
get {
return UserDefaults.standard.string(forKey: languageKey)
?? Locale.preferredLanguages.first
?? "en"
}
set {
UserDefaults.standard.set(newValue, forKey: languageKey)
UserDefaults.standard.synchronize()
}
}
func setLanguage(_ languageCode: String) {
currentLanguage = languageCode
// ์ฑ ์ฌ์์ ํ์ ์๋ฆผ
NotificationCenter.default.post(
name: NSNotification.Name("LanguageChanged"),
object: nil
)
}
func localizedString(for key: String, comment: String = "") -> String {
guard let path = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
let bundle = Bundle(path: path) else {
return NSLocalizedString(key, comment: comment)
}
return bundle.localizedString(forKey: key, value: nil, table: nil)
}
}
// ์ธ์ด ์ ํ ํ๋ฉด
class LanguageSettingsViewController: UIViewController {
private let languages = [
("en", "English", "๐บ๐ธ"),
("ko", "ํ๊ตญ์ด", "๐ฐ๐ท"),
("ja", "ๆฅๆฌ่ช", "๐ฏ๐ต"),
("zh-Hans", "็ฎไฝไธญๆ", "๐จ๐ณ"),
("es", "Espaรฑol", "๐ช๐ธ")
]
private let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
// ์ธ์ด ๋ณ๊ฒฝ ๊ฐ์ง
NotificationCenter.default.addObserver(
self,
selector: #selector(languageChanged),
name: NSNotification.Name("LanguageChanged"),
object: nil
)
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "LanguageCell")
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
@objc private func languageChanged() {
// ์ฑ ์ฌ์์ ์๋ด
let alert = UIAlertController(
title: LanguageManager.shared.localizedString(for: "language_changed_title"),
message: LanguageManager.shared.localizedString(for: "language_changed_message"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(
title: LanguageManager.shared.localizedString(for: "restart_now"),
style: .default
) { _ in
// ์ฑ ์ฌ์์ (์ค์ ๋ก๋ ๋ฃจํธ ๋ทฐ ์ปจํธ๋กค๋ฌ ๊ต์ฒด)
self.restartApp()
})
present(alert, animated: true)
}
private func restartApp() {
guard let window = UIApplication.shared.windows.first else { return }
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootVC = storyboard.instantiateInitialViewController()
window.rootViewController = rootVC
UIView.transition(with: window,
duration: 0.3,
options: .transitionCrossDissolve,
animations: nil,
completion: nil)
}
}
extension LanguageSettingsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return languages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LanguageCell", for: indexPath)
let language = languages[indexPath.row]
cell.textLabel?.text = "\(language.2) \(language.1)"
// ํ์ฌ ์ ํ๋ ์ธ์ด ํ์
if LanguageManager.shared.currentLanguage.hasPrefix(language.0) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let language = languages[indexPath.row]
LanguageManager.shared.setLanguage(language.0)
tableView.reloadData()
}
}
2. ์ฑ ๋ด ์ธ์ด ์ ํ์ ์ ๊ณตํ๋ค๋ฉด, ์ค์ ์ ๋ช ํํ ๊ณณ์ ๋ฐฐ์นํด
3. ์ธ์ด ๋ณ๊ฒฝ ํ ์ฑ ์ฌ์์์ด ํ์ํ๋ค๋ฉด ์ฌ์ฉ์์๊ฒ ๋ช ํํ ์๋ดํด
4. ์ผ๋ถ ํ๋ฉด๋ง ์๋ก๊ณ ์นจํ๋ ๊ฒ๋ณด๋ค ์ ์ฒด ์ฑ์ ์ฌ์์ํ๋ ๊ฒ ์์ ํด
๐ ์ค์ ํ๋ก์ ํธ: ๋ค๊ตญ์ด ์ผํ๋ชฐ ์ฑ
์, ์ด์ ๋ฐฐ์ด ๊ฑธ ๋ชจ๋ ํ์ฉํด์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ ์ผํ๋ชฐ ์ฑ์ ์ํ ์์ธ ํ๋ฉด์ ๋ค๊ตญ์ด๋ก ๊ตฌํํด๋ณผ๊ฒ. ๐
์ด ์์ ๋ ์ฌ๋ฅ๋ท์์ ํ๋ก์ ํธ๋ฅผ ๋ฐ์์ ๋ ์ค์ ๋ก ๊ตฌํํ ์ ์๋ ์์ค์ ์ฝ๋์ผ! ๐ผ
๐ฑ ํ๋ก์ ํธ ๊ตฌ์กฐ
struct Product {
let id: String
let nameKey: String // Localizable.strings์ ํค
let descriptionKey: String
let price: Double
let currency: String
let imageURL: String
let stock: Int
let rating: Double
let reviewCount: Int
}
// ์ํ ๋ฐ์ดํฐ
extension Product {
static let sampleProducts = [
Product(
id: "1",
nameKey: "product_smartphone_name",
descriptionKey: "product_smartphone_desc",
price: 999.99,
currency: "USD",
imageURL: "smartphone.jpg",
stock: 15,
rating: 4.5,
reviewCount: 1234
),
Product(
id: "2",
nameKey: "product_laptop_name",
descriptionKey: "product_laptop_desc",
price: 1499.99,
currency: "USD",
imageURL: "laptop.jpg",
stock: 8,
rating: 4.8,
reviewCount: 856
)
]
}
class ProductDetailViewController: UIViewController {
// MARK: - Properties
private var product: Product
// MARK: - UI Components
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.showsVerticalScrollIndicator = true
return scroll
}()
private let contentView = UIView()
private let productImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.layer.cornerRadius = 12
imageView.clipsToBounds = true
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.numberOfLines = 0
return label
}()
private let priceLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 28, weight: .heavy)
label.textColor = .systemRed
return label
}()
private let ratingStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 8
stack.alignment = .center
return stack
}()
private let stockLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .systemGray
return label
}()
private let descriptionTitleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .semibold)
return label
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.numberOfLines = 0
label.textColor = .darkGray
return label
}()
private let addToCartButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
button.layer.cornerRadius = 12
return button
}()
private let buyNowButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemGreen
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
button.layer.cornerRadius = 12
return button
}()
// MARK: - Initialization
init(product: Product) {
self.product = product
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureWithProduct()
setupActions()
}
// MARK: - Setup
private func setupUI() {
view.backgroundColor = .systemBackground
view.addSubview(scrollView)
scrollView.addSubview(contentView)
[productImageView, nameLabel, priceLabel, ratingStackView,
stockLabel, descriptionTitleLabel, descriptionLabel,
addToCartButton, buyNowButton].forEach {
contentView.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
productImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
productImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
productImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
productImageView.heightAnchor.constraint(equalToConstant: 300),
nameLabel.topAnchor.constraint(equalTo: productImageView.bottomAnchor, constant: 20),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
priceLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 12),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
ratingStackView.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 12),
ratingStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
stockLabel.topAnchor.constraint(equalTo: ratingStackView.bottomAnchor, constant: 8),
stockLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
descriptionTitleLabel.topAnchor.constraint(equalTo: stockLabel.bottomAnchor, constant: 24),
descriptionTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
descriptionTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
descriptionLabel.topAnchor.constraint(equalTo: descriptionTitleLabel.bottomAnchor, constant: 12),
descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
addToCartButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30),
addToCartButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
addToCartButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
addToCartButton.heightAnchor.constraint(equalToConstant: 50),
buyNowButton.topAnchor.constraint(equalTo: addToCartButton.bottomAnchor, constant: 12),
buyNowButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
buyNowButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
buyNowButton.heightAnchor.constraint(equalToConstant: 50),
buyNowButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
])
}
private func configureWithProduct() {
// ์ํ ์ด๋ฆ (์ง์ญํ)
nameLabel.text = NSLocalizedString(product.nameKey, comment: "์ํ ์ด๋ฆ")
// ๊ฐ๊ฒฉ (ํตํ ํ์ ์ง์ญํ)
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = product.currency
formatter.locale = Locale.current
priceLabel.text = formatter.string(from: NSNumber(value: product.price))
// ํ์ ๋ฐ ๋ฆฌ๋ทฐ ์
setupRatingView()
// ์ฌ๊ณ (๋ณต์ํ ์ฒ๋ฆฌ)
let stockFormat = NSLocalizedString("product_stock_count", comment: "์ฌ๊ณ ์๋")
stockLabel.text = String.localizedStringWithFormat(stockFormat, product.stock)
// ์ค๋ช
์ ๋ชฉ
descriptionTitleLabel.text = NSLocalizedString("product_description_title",
comment: "์ํ ์ค๋ช
์ ๋ชฉ")
// ์ํ ์ค๋ช
(์ง์ญํ)
descriptionLabel.text = NSLocalizedString(product.descriptionKey,
comment: "์ํ ์ค๋ช
")
// ๋ฒํผ ํ
์คํธ
addToCartButton.setTitle(NSLocalizedString("product_add_to_cart",
comment: "์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ ๋ฒํผ"),
for: .normal)
buyNowButton.setTitle(NSLocalizedString("product_buy_now",
comment: "๋ฐ๋ก ๊ตฌ๋งค ๋ฒํผ"),
for: .normal)
// ๋ค๋น๊ฒ์ด์
ํ์ดํ
navigationItem.title = NSLocalizedString("product_detail_title",
comment: "์ํ ์์ธ ํ๋ฉด ์ ๋ชฉ")
}
private func setupRatingView() {
// ๋ณ์ ํ์
let starsLabel = UILabel()
starsLabel.text = String(repeating: "โญ๏ธ", count: Int(product.rating))
starsLabel.font = .systemFont(ofSize: 16)
// ํ์ ์ซ์
let ratingLabel = UILabel()
ratingLabel.text = String(format: "%.1f", product.rating)
ratingLabel.font = .systemFont(ofSize: 16, weight: .semibold)
// ๋ฆฌ๋ทฐ ์ (๋ณต์ํ ์ฒ๋ฆฌ)
let reviewLabel = UILabel()
let reviewFormat = NSLocalizedString("product_review_count", comment: "๋ฆฌ๋ทฐ ์")
reviewLabel.text = "(\(String.localizedStringWithFormat(reviewFormat, product.reviewCount)))"
reviewLabel.font = .systemFont(ofSize: 14)
reviewLabel.textColor = .systemGray
ratingStackView.addArrangedSubview(starsLabel)
ratingStackView.addArrangedSubview(ratingLabel)
ratingStackView.addArrangedSubview(reviewLabel)
}
private func setupActions() {
addToCartButton.addTarget(self,
action: #selector(addToCartTapped),
for: .touchUpInside)
buyNowButton.addTarget(self,
action: #selector(buyNowTapped),
for: .touchUpInside)
}
// MARK: - Actions
@objc private func addToCartTapped() {
let message = NSLocalizedString("product_added_to_cart",
comment: "์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ ์๋ฃ ๋ฉ์์ง")
showAlert(message: message)
}
@objc private func buyNowTapped() {
let title = NSLocalizedString("purchase_confirm_title",
comment: "๊ตฌ๋งค ํ์ธ ์ ๋ชฉ")
let message = String(format: NSLocalizedString("purchase_confirm_message",
comment: "๊ตฌ๋งค ํ์ธ ๋ฉ์์ง"),
priceLabel.text ?? "")
let alert = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(
title: NSLocalizedString("common_confirm", comment: "ํ์ธ"),
style: .default
) { [weak self] _ in
self?.processPurchase()
})
alert.addAction(UIAlertAction(
title: NSLocalizedString("common_cancel", comment: "์ทจ์"),
style: .cancel
))
present(alert, animated: true)
}
private func processPurchase() {
let message = NSLocalizedString("purchase_success",
comment: "๊ตฌ๋งค ์ฑ๊ณต ๋ฉ์์ง")
showAlert(message: message)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: nil,
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(
title: NSLocalizedString("common_ok", comment: "ํ์ธ"),
style: .default
))
present(alert, animated: true)
}
}
๐ ๋ฒ์ญ ํ์ผ
/* Common */
"common_ok" = "OK";
"common_cancel" = "Cancel";
"common_confirm" = "Confirm";
/* Product Detail */
"product_detail_title" = "Product Details";
"product_description_title" = "Description";
"product_add_to_cart" = "Add to Cart";
"product_buy_now" = "Buy Now";
"product_added_to_cart" = "Product added to cart successfully!";
/* Product Info */
"product_smartphone_name" = "Premium Smartphone X";
"product_smartphone_desc" = "Experience the latest technology with our flagship smartphone. Features include a stunning display, powerful processor, and advanced camera system.";
"product_laptop_name" = "Professional Laptop Pro";
"product_laptop_desc" = "Perfect for professionals and creators. High-performance processor, brilliant display, and all-day battery life.";
/* Purchase */
"purchase_confirm_title" = "Confirm Purchase";
"purchase_confirm_message" = "Do you want to purchase this item for %@?";
"purchase_success" = "Purchase completed successfully!";
/* Common */
"common_ok" = "ํ์ธ";
"common_cancel" = "์ทจ์";
"common_confirm" = "ํ์ธ";
/* Product Detail */
"product_detail_title" = "์ํ ์์ธ";
"product_description_title" = "์ํ ์ค๋ช
";
"product_add_to_cart" = "์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ";
"product_buy_now" = "๋ฐ๋ก ๊ตฌ๋งค";
"product_added_to_cart" = "์ํ์ด ์ฅ๋ฐ๊ตฌ๋์ ๋ด๊ฒผ์ต๋๋ค!";
/* Product Info */
"product_smartphone_name" = "ํ๋ฆฌ๋ฏธ์ ์ค๋งํธํฐ X";
"product_smartphone_desc" = "์ต์ ๊ธฐ์ ์ด ์ง์ฝ๋ ํ๋๊ทธ์ญ ์ค๋งํธํฐ์ ๊ฒฝํํ์ธ์. ๋๋ผ์ด ๋์คํ๋ ์ด, ๊ฐ๋ ฅํ ํ๋ก์ธ์, ๊ทธ๋ฆฌ๊ณ ์ง๋ณด๋ ์นด๋ฉ๋ผ ์์คํ
์ ๊ฐ์ถ๊ณ ์์ต๋๋ค.";
"product_laptop_name" = "ํ๋กํ์
๋ ๋
ธํธ๋ถ Pro";
"product_laptop_desc" = "์ ๋ฌธ๊ฐ์ ํฌ๋ฆฌ์์ดํฐ๋ฅผ ์ํ ์๋ฒฝํ ์ ํ. ๊ณ ์ฑ๋ฅ ํ๋ก์ธ์, ์ ๋ช
ํ ๋์คํ๋ ์ด, ๊ทธ๋ฆฌ๊ณ ํ๋ฃจ ์ข
์ผ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฐฐํฐ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.";
/* Purchase */
"purchase_confirm_title" = "๊ตฌ๋งค ํ์ธ";
"purchase_confirm_message" = "%@ ์ํ์ ๊ตฌ๋งคํ์๊ฒ ์ต๋๊น?";
"purchase_success" = "๊ตฌ๋งค๊ฐ ์๋ฃ๋์์ต๋๋ค!";
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>product_stock_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@stock@</string>
<key>stock</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Out of stock</string>
<key>one</key>
<string>Only 1 left in stock</string>
<key>other</key>
<string>%d items in stock</string>
</dict>
</dict>
<key>product_review_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@reviews@</string>
<key>reviews</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No reviews</string>
<key>one</key>
<string>1 review</string>
<key>other</key>
<string>%d reviews</string>
</dict>
</dict>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>product_stock_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@stock@</string>
<key>stock</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>ํ์ </string>
<key>other</key>
<string>์ฌ๊ณ %d๊ฐ</string>
</dict>
</dict>
<key>product_review_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@reviews@</string>
<key>reviews</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>๋ฆฌ๋ทฐ ์์</string>
<key>other</key>
<string>๋ฆฌ๋ทฐ %d๊ฐ</string>
</dict>
</dict>
</dict>
</plist>
์ด ์์ ๋ ์ค์ ํ๋ก์ ํธ์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ ์์ค์ด์ผ! ๐
ํนํ ์ฃผ๋ชฉํ ์ :
โ
๋ชจ๋ UI ํ
์คํธ๊ฐ ์ง์ญํ๋์ด ์์ด
โ
๊ฐ๊ฒฉ์ ์ฌ์ฉ์์ ๋ก์ผ์ผ์ ๋ง๊ฒ ํ์๋ผ
โ
์ฌ๊ณ ์ ๋ฆฌ๋ทฐ ์๋ ๋ณต์ํ ๊ท์น์ด ์ ์ฉ๋ผ
โ
Auto Layout์ผ๋ก RTL ์ธ์ด๋ ์๋ ๋์
โ
์ฝ๋๊ฐ ๊น๋ํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๊ณ ์ ๋ง์์ด! ๐
iOS ๊ตญ์ ํ์ NSLocalizedString์ ๋ํด ์ ๋ง ๋ง์ ๊ฑธ ๋ฐฐ์ ์ง? ์ด์ ๋๋ ์ ์ธ๊ณ ์ฌ์ฉ์๋ค์ด ์ฌ์ฉํ ์ ์๋ ์ฑ์ ๋ง๋ค ์ ์์ด! ๐โจ
๋ง์ง๋ง์ผ๋ก ํต์ฌ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณผ๊ฒ:
- ๊ธ๋ก๋ฒ ์์ฅ ์ง์ถ์ ํ์ ์์
- ์ฌ์ฉ์ ๊ฒฝํ ํฅ์
- ๋ค์ด๋ก๋ ์ ์ฆ๊ฐ
2. NSLocalizedString ๊ธฐ๋ณธ
- ๋ฌธ์์ด ์ง์ญํ์ ํต์ฌ ๋๊ตฌ
- Localizable.strings ํ์ผ ํ์ฉ
- Comment๋ก ๋ฒ์ญ๊ฐ ์ง์
3. ๋ณต์ํ ์ฒ๋ฆฌ
- Stringsdict ํ์ผ ์ฌ์ฉ
- ์ธ์ด๋ณ ๋ณต์ํ ๊ท์น ์ ์ฉ
- String.localizedStringWithFormat ํ์ฉ
4. RTL ์ธ์ด ์ง์
- Auto Layout ํ์ฉ
- leading/trailing ์ฌ์ฉ
- UIStackView๋ก ๊ฐํธํ๊ฒ
5. ๋ฒ ์คํธ ํ๋ํฐ์ค
- ๊ตฌ์กฐํ๋ ํค ๊ด๋ฆฌ
- ๋ฒ์ญ ํ์ง ๊ด๋ฆฌ
- ์ง์์ ์ธ ํ ์คํธ
์์ผ๋ก ์ฑ์ ๊ฐ๋ฐํ ๋๋ ์ฒ์๋ถํฐ ๊ตญ์ ํ๋ฅผ ์ผ๋์ ๋๊ณ ์ค๊ณํ๋ ๊ฒ ์ข์. ๋์ค์ ์ถ๊ฐํ๋ ค๋ฉด ์ ๋ง ํ๋ค๊ฑฐ๋ ! ๐
๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ๊ตญ์ ํ ๊ด๋ จ ํ๋ก์ ํธ๋ฅผ ๋ฐ๊ฑฐ๋, ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ๊ฒฝํ์ ๊ณต์ ํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ์ค์ ๊ฒฝํ์ด ๊ฐ์ฅ ์ค์ํ๋๊น! ๐ช
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด:
๐ Apple ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ
๐ Stack Overflow ๊ฒ์
๐ฅ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ ํ์ฉ
๐ก ์ค์ ๋ก ๋ง๋ค์ด๋ณด๋ฉฐ ํ์ต
์, ์ด์ ๋๋ง์ ๋ฉ์ง ๋ค๊ตญ์ด ์ฑ์ ๋ง๋ค์ด๋ด! ํ์ดํ
! ๐โจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.