๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/Architecture

[RIBs] Interactor๋กœ ๋น„๋‹ˆ์ง€์Šค ๋กœ์ง ์ฒ˜๋ฆฌํ•ด๋ณด๊ธฐ (feat. ์ดˆ๊ธฐ์„ธํŒ…)

by Fomagran ๐Ÿ’ป 2021. 8. 18.
728x90
๋ฐ˜์‘ํ˜•

 

์•ˆ๋…•ํ•˜์„ธ์š” Foma ๐Ÿ‘Ÿ ์ž…๋‹ˆ๋‹ค!

 

์ €๋ฒˆ ๊ธ€์—์„œ RIBs ์— ๋Œ€ํ•œ ์ด๋ก ์„ ๋‹ค๋ค˜๋Š”๋ฐ์š”.

 

(ํ˜น์‹œ ์•ˆ๋ณด์‹  ๋ถ„๋“ค์€ ์—ฌ๊ธฐ ์—์„œ ๋ณด๊ณ  ์™€์ฃผ์„ธ์š”~)

 

์˜ค๋Š˜์€ Uber์—์„œ ์ง์ ‘ ์ œ๊ณตํ•˜๋Š” ํŠœํ† ๋ฆฌ์–ผ์„ ํ•œ๋ฒˆ ๋”ฐ๋ผํ•ด๋ณด๋ฉด์„œ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค!

 

๋ฐ”๋กœ ์‹œ์ž‘ํ• ๊ฒŒ์š”~


 

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

 

๊ฐ€์žฅ ๋จผ์ € RIBs ํŠœํ† ๋ฆฌ์–ผ์„ ๋”ฐ๋ผํ•  ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ค˜์•ผ๊ฒ ์ฃ ?

 

์ €๋Š” RIBs Example๋กœ ์ด๋ฆ„ ์ง“๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 


Pod init & install

 

์ €๋Š” CocoaPod์„ ์ด์šฉํ•ด์„œ RIBs๋ฅผ ์„ค์น˜ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

ํ„ฐ๋ฏธ๋„์—์„œ ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•ด pod init์„ ํ•ด์ฃผ์‹œ๋ฉด pod file์ด ์ƒ๊ธธ๊ฑฐ์—์š”.

 

ํŒŸํŒŒ์ผ์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (๋ฒ„์ „์€ ๋ฐ”๋€” ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ณต์‹ ๊นƒํ—™์—์„œ ํ™•์ธํ•ด์ฃผ์„ธ์š”!)

pod 'RIBs', '~> 0.9'

 

์ถ”๊ฐ€ํ•œ ๋‹ค์Œ pod install์„ ํ•ด์ค˜ RIBs๋ฅผ ์„ค์น˜ํ•ด์ค๋‹ˆ๋‹ค.


ํ”„๋ ˆ์ž„์›Œํฌ ์„ค์น˜

 

RIBs๋Š” ์ž์ฒด ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ Xcode์•ˆ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์–ด์กŒ์Šต๋‹ˆ๋‹ค.

 

RIBs ๊ณต์‹ ๊นƒํ—™์œผ๋กœ ์ด๋™ํ•ด์ฃผ์„ธ์š”.

 

 

GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.

Uber's cross-platform mobile architecture framework. - GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.

github.com

 

Download ZIP์„ ๋ˆŒ๋Ÿฌ์„œ ๋‹ค์šด๋ฐ›์•„ ์ฃผ์„ธ์š”.

 

 

ํ„ฐ๋ฏธ๋„๋กœ ๋‹ค์‹œ ์ด๋™ํ•ด์„œ ๋‹ค์šด๋ฐ›์œผ์‹  ํŒŒ์ผ ์ค‘ RIBs-master/ios/tooling/install-xcode-template.sh๋ฅผ ๋“œ๋ž˜๊ทธ ํ•ด์„œ ๊ฐ€์ ธ๊ฐ€์ค๋‹ˆ๋‹ค.

 

 

์ด๋ ‡๊ฒŒ ์„ฑ๊ณต์ ์œผ๋กœ ์„ค์น˜๋˜์—ˆ๋‹ค๋ฉด success!๊ฐ€ ๋œฐ๊ฑฐ์—์š”.

 


Xcode

 

์—‘์Šค์ฝ”๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์‹œ๋Š”๋ฐ ์ฃผ์˜ํ•  ์ ์€ ํ”„๋กœ์ ํŠธ๋ช….xworkspace๋กœ ์‹คํ–‰์‹œ์ผœ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 


LoggedOut RIB

 

์ด์ œ RIB ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ƒ์„ฑํ•ด๋ณผ๊นŒ์š”?

 

์ƒˆ๋กœ์šด ํŒŒ์ผ ์ƒ์„ฑ์„ ๋ˆ„๋ฅด๊ณ  ์•„๋ž˜๋กœ ์ญ‰ ๋‚ด๋ฆฌ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด RIB ํŒŒ์ผ๋“ค์ด ์žˆ์„๊ฑฐ์—์š”.

 

 

Storyboard๋‚˜ XIB๋ฅผ ๋งŒ๋“œ์‹ค๊ฑฐ๋ฉด ์•„๋ž˜์—์„œ Adds XIB file or Adds Storyboard file์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”.

 

(์ €๋Š” ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ๋„ฃ์–ด์คฌ์Šต๋‹ˆ๋‹ค.)

 

์ด๋ฆ„์€ LoggedOut์œผ๋กœ ์ง€์–ด์ฃผ์„ธ์š” (ํŠœํ† ๋ฆฌ์–ผ์— ๊ทธ๋ ‡๊ฒŒ ๋˜์–ด์žˆ์Œ)

 

 

์ด๋ ‡๊ฒŒํ•˜๋ฉด Router,ViewController,Builder,Interactor,Storyboard 5๊ฐœ์˜ ํŒŒ์ผ์ด ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

 

 

๊ฐ€์žฅ ๋จผ์ € ์Šคํ† ๋ฆฌ๋ณด๋“œ๋กœ ์ด๋™ํ•ด์„œ LoggedOut ๋ ˆ์ด์•„์›ƒ์„ ํ•ด์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

ํ”Œ๋ ˆ์ด์–ด1,ํ”Œ๋ ˆ์ด์–ด2์˜ ์ด๋ฆ„์„ ์ ์„ ํ…์ŠคํŠธํ•„๋“œ 2๊ฐœ์™€ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํ•˜๋‚˜๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

 

๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์— ์—ฐ๊ฒฐํ•ด์ฃผ๊ณ  Login ๋ฒ„ํŠผ์„ ํƒญํ–ˆ์„ ๋•Œ ์•ก์…˜๋„ ์—ฐ๊ฒฐํ•ด์ค๋‹ˆ๋‹ค.

 

import RIBs
import RxSwift
import UIKit

protocol LoggedOutPresentableListener: AnyObject {
}

final class LoggedOutViewController: UIViewController, LoggedOutPresentable, LoggedOutViewControllable {
    
    @IBOutlet weak var player1NameTF: UITextField!
    @IBOutlet weak var player2NameTF: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    weak var listener: LoggedOutPresentableListener?
    
    @IBAction func tapLoginButton(_ sender: Any) {
    }
}

 

์œ„์ชฝ์— LoggedOutPresentableListener ํ”„๋กœํ† ์ฝœ์ด ์žˆ์„๊ฑฐ์—์š”.

 

์ด ๋ฆฌ์Šค๋„ˆ๋Š” ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ผ์–ด๋‚œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋กœ์ง ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

๋กœ๊ทธ์ธ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

protocol LoggedOutPresentableListener: AnyObject {
    func handleLogin(player1Name:String,player2Name:String)
}

 

๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๋ฆฌ์Šค๋„ˆ์˜ ํ•ธ๋“ค๋กœ๊ทธ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด์ค๋‹ˆ๋‹ค.

 

ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ๋Š” ํ”Œ๋ ˆ์ด์–ด1์˜ ์ด๋ฆ„, ํ”Œ๋ ˆ์ด์–ด2์˜ ์ด๋ฆ„์„ ๋„˜๊ฒจ์ฃผ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

 @IBAction func tapLoginButton(_ sender: Any) {
        listener?.handleLogin(player1Name: player1NameTF.text ?? "", player2Name: player2NameTF.text ?? "")
    }

 

์ €๋ฒˆ ๊ธ€์— ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์ด ์–ด๋””๋ผ๊ณ  ํ–ˆ์ฃ ?

 

๋ฐ”๋กœ ์ธํ„ฐ๋ž™ํ„ฐ ์ž…๋‹ˆ๋‹ค.

 

LoggedOutInteractor๋กœ ์ด๋™ํ•ด์ค๋‹ˆ๋‹ค.

 

์ธํ„ฐ๋ž™ํ„ฐ ๋ ๋ถ€๋ถ„์— LoggedOutPresentableListener์—์„œ ์ •์˜ํ•œ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ด์ค๋‹ˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ ํ•จ์ˆ˜ ์ด๋ฆ„์„ ์กฐ๊ธˆ๋งŒ ์ณ๋„ ์ž๋™ ์™„์„ฑ์ด ๋ ๊ฑฐ์—์š”!

 

 

๊ฐ„๋‹จํ•˜๊ฒŒ ํ”Œ๋ ˆ์ด์–ด1,ํ”Œ๋ ˆ์ด์–ด2์˜ ์ด๋ฆ„์„ ์ถœ๋ ฅ๋งŒ ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

final class LoggedOutInteractor: PresentableInteractor<LoggedOutPresentable>, LoggedOutInteractable, LoggedOutPresentableListener {

    weak var router: LoggedOutRouting?
    weak var listener: LoggedOutListener?

    ...
    
    func handleLogin(player1Name: String, player2Name: String) {
        print(player1Name,player2Name)
    }
}

 

 

์ด๋ ‡๊ฒŒ ํ•˜๊ณ  ์‹คํ–‰์„ ๋ˆ„๋ฅด๋ฉด ๊นŒ๋งŒ ํ™”๋ฉด๋งŒ ๋‚˜์˜ฌ๊ฑฐ์—์š”.

 

์™œ๋ƒํ•˜๋ฉด ์ดˆ๊ธฐ ํ™”๋ฉด์„ ์ •ํ•ด์ฃผ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด์ฃ .


 

RootRIB

 

์ดˆ๊ธฐRIB์„ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

์ด๋ฆ„์€ Root๋กœ ํ• ๊ฒŒ์š”.

 

 

RootBuilder

 

Builder๋Š” RIB์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

 

๊ณ ๋กœ RootBuilder์—์„œ ์ž์‹์ธ LoggedOutRIB์„ ์ƒ์„ฑํ•ด์ค˜์•ผ ๋˜๊ฒ ์ฃ ?

 

build ๋ฉ”์†Œ๋“œ์— LoggedOut๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ 2๊ฐ€์ง€ ์˜ค๋ฅ˜๊ฐ€ ๋‚ ๊ฑฐ์—์š”.

 

ํ•˜๋‚˜๋Š” component์— ๋Œ€ํ•œ ๊ฒƒ์ธ๋ฐ ์•„์ง RootComponent์— LoggedOutDependency๋ฅผ ์ฃผ์ž…ํ•ด์ฃผ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๊ณ ,

 

ํ•˜๋‚˜๋Š” RootRouter์˜ ์ดˆ๊ธฐํ™”์—์„œ ์•„์ง LoggedOutBuilder๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด์—์š”.

 

func build() -> LaunchRouting {
           let viewController = RootViewController()
           let component = RootComponent(dependency: dependency,
                                         rootViewController: viewController)
           let interactor = RootInteractor(presenter: viewController)

           let loggedOutBuilder = LoggedOutBuilder(dependency: component)
        
           return RootRouter(interactor: interactor,
                             viewController: viewController,
                             loggedOutBuilder: loggedOutBuilder)
       }

 

RootComponent + LoggedOut

 

Component๋Š” ํ•ด๋‹น RIB์˜ Dependency ๊ด€๋ฆฌํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

 

์•„๋ž˜์™€ ๊ฐ™์ด RootComponent์— LoggedOutDependency๋ฅผ ์ฑ„ํƒํ•ด์ค๋‹ˆ๋‹ค.

 

import RIBs

protocol RootDependencyLoggedOut: Dependency {
}

extension RootComponent: LoggedOutDependency {
}

 

RootRouter

 

RootRouter์—์„œ loggedOutBuilder๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์ค๋‹ˆ๋‹ค.

 

final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
	
    private let loggedOutBuilder: LoggedOutBuildable
    
    init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loggedOutBuilder: LoggedOutBuildable) {
        
        self.loggedOutBuilder = loggedOutBuilder
        
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }

 

์ด ๋•Œ LoggedOutRIB์—์„œ ์ผ์–ด๋‚˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ RootInteractable ํ”„๋กœํ† ์ฝœ์— LoggedOutListener๋ฅผ ์ฑ„ํƒํ•ด์ค๋‹ˆ๋‹ค.

 

protocol RootInteractable: Interactable, LoggedOutListener,LoggedInListener {
    var router: RootRouting? { get set }
    var listener: RootListener? { get set }
}

 

RootViewController

 

๋‹ค๋งŒ RootRIB์ด๋ผ๋Š” ๊ฒƒ์„ ์•Œ์•„์ฑŒ ์ˆ˜ ์žˆ๊ฒŒ ๋ ˆ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

 

let rootLabel:UILabel = UILabel()

 

๋ ˆ์ด์•„์›ƒ์„ ํ•ด์ฃผ์‹œ๊ณ  

 

  func layout() {
        self.view.backgroundColor = .white
        
        rootLabel.text = "์—ฌ๊ธฐ๋Š” ๋ฃจํŠธ๋ฆฝ์ž…๋‹ˆ๋‹ค."
        view.addSubview(rootLabel)
        rootLabel.translatesAutoresizingMaskIntoConstraints = false
        rootLabel.centerXAnchor.constraint(equalTo:view.centerXAnchor)
                    .isActive = true
        rootLabel.centerYAnchor.constraint(equalTo:view.centerYAnchor)
                    .isActive = true
    }

 

๋ทฐ๋””๋“œ๋กœ๋“œ์— ์‹คํ–‰์‹œ์ผœ์ค๋‹ˆ๋‹ค.

 

 override func viewDidLoad() {
        super.viewDidLoad()
        layout()
    }

SceneDelegate

 

์”ฌ๋”œ๋ฆฌ๊ฒŒ์ดํŠธ๋กœ ์ด๋™ํ•ด์„œ RIBs๋ฅผ import ํ•ด์ฃผ๊ณ  window์™€ launchRouter๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

๊ทธ ๋‹ค์Œ scene ๋ฉ”์†Œ๋“œ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๋ถ™์—ฌ๋„ฃ์–ด์ฃผ์„ธ์š”.

 

(๋Œ€์ถฉ RootBuilder๋กœ ์ดˆ๊ธฐ๋ฅผ ์„ค์ •ํ•œ๋‹ค๋Š” ๋‚ด์šฉ๊ฐ™์•„์š”)

 

import UIKit
import RIBs

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    private var launchRouter: LaunchRouting?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: windowScene)
        self.window = window
        let launchRouter = RootBuilder(dependency: AppComponent()).build()
        self.launchRouter = launchRouter
        launchRouter.launchFromWindow(window)
    }
}

AppComponent

 

์œ„ ์”ฌ๋”œ๋ฆฌ๊ฒŒ์ดํŠธ์—์„œ AppComponent ๋ถ€๋ถ„์— ์—๋Ÿฌ๊ฐ€ ๋‚ ๊ฑฐ์—์š”.

 

์•ฑ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

import RIBs

class AppComponent: Component<EmptyDependency>, RootDependency {
    init() {
        super.init(dependency: EmptyComponent())
    }
}

์ด์ œ ์•ฑ์„ ์‹คํ–‰์‹œ์ผœ๋ณด๋ฉด?

 

์•„์ง๋„ ๊ฒ€์ •ํ™”๋ฉด ์ผ๊ฑฐ์—์š”.. (ํ›„..ํŠœํ† ๋ฆฌ์–ผ ํ•˜๊ธฐ ํž˜๋“œ๋„ค..)

 

 

 

๊ธฐ์กด์— ์žˆ๋˜ ViewController์™€ Main.storyboard ํŒŒ์ผ์„ ์ง€์›Œ๋ฒ„๋ฆฌ๊ณ 

 

Target - info๋กœ ์ด๋™ํ•ด์ค๋‹ˆ๋‹ค.

 

Application Scene Manifest - Scene Configuration - Application Session Role - Item 0 - Storyboard Name

 

์„ ์ง€์›Œ์ฃผ์„ธ์š”!

 

๊ทธ๋ฆฌ๊ณ  Main storyboard file base name์ด ์žˆ์„๊ฑฐ์—์š”.

 

์ด๊ฒƒ๋„ ์ง€์›Œ์ค๋‹ˆ๋‹ค

 

 

๊ทธ๋ฆฌ๊ณ  ์‹คํ–‰ํ•ด์ฃผ์‹œ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด LoggedOutViewController๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค... ๋“œ๋””์–ด...!

 

์ด์ œ ํ”Œ๋ ˆ์ด์–ด1 ์ด๋ฆ„๊ณผ ํ”Œ๋ ˆ์ด์–ด2 ์ด๋ฆ„์„ ์ ๊ณ  Login์„ ํ•ด์ฃผ์‹œ๋ฉด 

 

 

 

์•„๋ž˜์™€ ๊ฐ™์ด ํ”Œ๋ ˆ์ด์–ด1,ํ”Œ๋ ˆ์ด์–ด2 ์ด๋ฆ„์ด ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!!

 


์ด๋ฒˆ ํŠœํ† ๋ฆฌ์–ผ์„ ํ†ตํ•ด์„œ ViewController์—์„œ ์ผ์–ด๋‚œ ์ด๋ฒคํŠธ๋“ค์„ Listener๋กœ ๊ฐ์ง€ํ•˜๊ณ  Interactor์— ์ „๋‹ฌํ•˜์—ฌ

 

Interactor์—์„œ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ ์„ธํŒ…ํ•˜๊ณ  ํ•˜๋Š” ๊ฑฐ๋ผ ์–ด๋ ค์›€์ด ๋งŽ์•˜์ง€๋งŒ ํŠœํ† ๋ฆฌ์–ผ ํ•˜๋‚˜๋ฅผ ๋๋‚ด๋‹ˆ ๋ฟŒ๋“ฏํ•˜๋„ค์š”..

 

ํ˜น์‹œ๋ผ๋„ ๊ถ๊ธˆํ•˜์‹  ์ ์ด๋‚˜ ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”!

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€