apple/UIKit & ReactiveX

Ch13. 🦕 Intermediate RxCocoa

lgvv 2021. 8. 12. 15:20

Ch13. 🦕 Intermediate RxCocoa

RxSwiftExample-master.zip
8.54MB

✅ 내가 돌아왔다!

 예전에 rx를 처음 공부할 때는 이 부분 하나도 이해 안가서 아래 사진처럼 해두고 스킵했었는데, 이제는 다 이해해서 포스팅 하러 옴!

와,, 그 당시에는 이렇게나 어려웠다

✅ [APP UI]

APP UI

 

✅ CLLocationManager + Rx

import Foundation
import CoreLocation
import RxSwift
import RxCocoa

extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}

class RxCLLocationManagerDelegateProxy:DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
    static func registerKnownImplementations() {
        self.register { (parent) -> RxCLLocationManagerDelegateProxy in
            return RxCLLocationManagerDelegateProxy(locationManager: parent)
        }
    }

    public weak private(set) var locationManager: CLLocationManager?

    public init(locationManager: ParentObject) {
        self.locationManager = locationManager
        super.init(parentObject: locationManager, delegateProxy: RxCLLocationManagerDelegateProxy.self)
    }
}

extension Reactive where Base == CLLocationManager {
    public var delegate: DelegateProxy<CLLocationManager, CLLocationManagerDelegate> {
        return RxCLLocationManagerDelegateProxy.proxy(for: base)
    }

    var didUpdateLocations: Observable<[CLLocation]> {
        return delegate.methodInvoked(
            #selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:))
            ).map({ (params) in
                // PARAMS contains CLLocationManager and CLLocation array
                return params[1] as! [CLLocation]
            })
    }

    var didGetLocationFail: Observable<Error> {
        return delegate.methodInvoked(
            #selector(CLLocationManagerDelegate.locationManager(_:didFailWithError:))
            ).map({ (params) in
                return params[1] as! Error
            })
    }
}

Proxy로 묶은 코드인데, 이 코드는 정말 Standard하게끔 묶고 있다.

 

 

✅ MKMapView+Rx

import Foundation
import MapKit
import RxSwift
import RxCocoa

extension MKMapView: HasDelegate {
    public typealias Delegate = MKMapViewDelegate
}

class RxMapViewDelegateProxy:DelegateProxy<MKMapView, MKMapViewDelegate>, DelegateProxyType, MKMapViewDelegate {
    public weak private(set) var mapView: MKMapView?

    static func registerKnownImplementations() {
        self.register { (parent) -> RxMapViewDelegateProxy in
            return RxMapViewDelegateProxy(mapView: parent)
        }
    }

    public init(mapView: ParentObject) {
        self.mapView = mapView
        super.init(parentObject: mapView, delegateProxy: RxMapViewDelegateProxy.self)
    }
}

extension Reactive where Base == MKMapView {
    public var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
        return RxMapViewDelegateProxy.proxy(for: base)
    }

    public func setDelegate(_ delegate: MKMapViewDelegate) -> Disposable {
        // 이 코드의 정확한 의미는 모르겠으나, 이 코드가 없다면 mapView에 날씨 아이콘이 나타나지 않는다.
        return RxMapViewDelegateProxy.installForwardDelegate(
            delegate,
            retainDelegate: false,
            onProxyForObject: self.base
        )
    }
    
    var overlays: Binder<[MKOverlay]> {
        return Binder(self.base){ mapView, overlays in
            mapView.removeOverlays(mapView.overlays)
            mapView.addOverlays(overlays)
        }
    }

    
    var regionDidChangeAnimated: ControlEvent<Bool> {
        //맵 보기에 표시된 영역이 바뀌었음을 delegate에게 알림
        let source = delegate.methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
            .map { (params) in
                return params[1] as? Bool ?? false
            }
        return ControlEvent(events: source)
    }

    var centerCoordinate: Binder<CLLocationCoordinate2D> {
        return Binder(self.base, binding: { (mapViews, center) in
            mapViews.centerCoordinate = center
        })
    }
}

 

여기도 정말 standard하게 묶고 있다. 눈에 띄는 점은 Binder와 ControlEvent를 사용했다는 것인데, 딱 봐도 어렵지 않으므로 쓱 읽어보도록 하자.

 

 

✅ ViewController

import UIKit
import RxSwift
import RxCocoa
import NSObject_Rx
import MapKit
import CoreLocation

class ViewController: UIViewController {
    
    @IBOutlet weak var mapView: MKMapView!
    @IBOutlet weak var mapButton: UIButton!
    @IBOutlet weak var geoLocationButton: UIButton! // 현재위치 버튼 - simulator에서는 san francisco로 되어있음
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var searchCityName: UITextField!
    @IBOutlet weak var tempLabel: UILabel!
    @IBOutlet weak var humidityLabel: UILabel!
    @IBOutlet weak var iconLabel: UILabel!
    @IBOutlet weak var cityNameLabel: UILabel!
    
    let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        style()
        
        // MARK: - GEO SEARCH
        locationManager.delegate = self
        
        let geoInput = geoLocationButton.rx.tap // 위치 아이콘 탭하면
            .do(onNext: { _ in
                self.locationManager.requestWhenInUseAuthorization()
                self.locationManager.requestLocation()
            })
                
        let currentLocation = locationManager.rx.didUpdateLocations // 위치 업데이트 되면 정보 받아옴
            .debug("currentLocation")
            .map { (locations) in
                return locations[0]
            }
        
        locationManager.rx.didGetLocationFail
            .subscribe(onNext: { error in
                print("Did fail with error", error.localizedDescription)
            })
            .disposed(by: rx.disposeBag)
        
        let geoLocation = geoInput.flatMap { // geoInput은 tap이벤트라 Void
            return currentLocation.take(1) // 현재위치로 업뎃
        } // 즉, 탭 이벤트 들어오면 현재위치로 바꿔서 geoLocation에 담기게 된다.
        
        let geoSearch = geoLocation.flatMap { (location) in // location은 Observable이다.
            ApiController.shared.currentWeather(
                lat: location.coordinate.latitude,
                lon: location.coordinate.longitude
            )
                .catchErrorJustReturn(ApiController.Weather.dummy)
        }
                
        // MARK: - MAP VIEW
        mapView.rx.setDelegate(self).disposed(by: rx.disposeBag)
        
        mapButton.rx.tap
            .subscribe(onNext: { [weak self] _ in
                guard let `self` = self else {
                    return
                }
                self.mapView.isHidden = !self.mapView.isHidden
            })
            .disposed(by: rx.disposeBag)
        
        let mapInput = mapView.rx.regionDidChangeAnimated // 화면에서 지도위치가 변경될 때 호출된다.
            .debug("mapInput")
            .skip(1)
            .map { _ in self.mapView.centerCoordinate } // 지금 보는 화면 mapView의 중심좌표를 반환하다
        
        let mapSearch = mapInput.flatMap { (coor) in // 지도 중심좌표를 가지고 그 좌표의 현재 날씨를 알아온다.
            return ApiController.shared.currentWeather(
                lat: coor.latitude,
                lon: coor.longitude
            )
                .catchErrorJustReturn(ApiController.Weather.dummy) // 에러 있으면 dummy로 처리하기
        }
        
        // MARK: - TEXT SEARCH
        let searchInput = searchCityName.rx.controlEvent(.editingDidEndOnExit)
            .asObservable()
            .map { self.searchCityName.text } // 터치이벤트라 void인데 터치 이벤트 날라오면 map을 통해서 text를 저장한다. 변수는 string타입
            .filter { ($0 ?? "").count > 0 } // map으로 바꿀 값이 0보다 더 큰 경우만
        
        let textSearch = searchInput
            .flatMap { text in // observable로 감싸있는 경우는 map이 아니라 flatmap을 사용해서 바꾼다.
                return ApiController.shared.currentWeather(city: text ?? "Error")
                    .catchErrorJustReturn(ApiController.Weather.dummy)
            } // Observable<Weather> 타입의 데이터를 갖고 있다.
        
        let search = Observable.from([
            geoSearch, // 현재 위치 버튼 클릭시, 위치의 좌표
            textSearch, // text입력 시, 텍스트 기준 좌표
            mapSearch // 지도 움직일 때, 지도 중심점 기준 좌표.
        ])
            .merge() // 세 옵저버블 합쳐서, merge를 통해 가장 마지막에 방출되는 이벤트 search값에 넣기
            .asDriver(onErrorJustReturn: ApiController.Weather.dummy) // 에러 시, dummy 내보내기
        
        // merge해서 그 데이터를 그에 알맞게끔 내려보낸다.
        
        search.map { "\($0.temperature)° C" }
        .drive(tempLabel.rx.text)
        .disposed(by: rx.disposeBag)
        
        search.map { $0.icon }
        .drive(iconLabel.rx.text)
        .disposed(by: rx.disposeBag)
        
        search.map { "\($0.humidity)%" }
        .drive(humidityLabel.rx.text)
        .disposed(by: rx.disposeBag)
        
        search.map { $0.cityName }
        .drive(cityNameLabel.rx.text)
        .disposed(by: rx.disposeBag)
        
        search.map {[$0.overlay()]}
        .drive(mapView.rx.overlays)
        .disposed(by: rx.disposeBag)
        
        let running = Observable.from([ // 얘는 데이터가 들어오면 그에 맞는 조건에 따른 이벤트들을 보여주기 위함.
            searchInput.map {_ in return true}, // 텍스트 입력
            geoInput.map {_ in return true}, // 현재위치 버튼 클릭
            mapInput.map {_ in return true}, // 맵 위치 이동
            search.map {_ in return false}.asObservable() // 결과가 나왔을 때
            // search의 경우에는 옵저버블이 여러겹 래핑되어 있어서 asObservable()을 통해 단일로 방출시킴
                                      ])
            .merge() // merge니까 running은 저기 위에 이벤트들은 전부 바라보고 있는데, 거기서 내려온 마지막 값을 방출받아서 할당해 사용하게 된다.
            .asDriver(onErrorJustReturn: false)
        
        // animatimg은 true여야 화면에 표시됨으로 애만 보이도록 하고 나머지는 끝나면 나타나게끔 처리한 로직인데 감탄!
        
        running.drive(activityIndicator.rx.isAnimating)
            .disposed(by: rx.disposeBag)
        running.drive(iconLabel.rx.isHidden)
            .disposed(by: rx.disposeBag)
        running.drive(tempLabel.rx.isHidden)
            .disposed(by: rx.disposeBag)
        running.drive(cityNameLabel.rx.isHidden)
            .disposed(by: rx.disposeBag)
        running.drive(humidityLabel.rx.isHidden)
            .disposed(by: rx.disposeBag)
        
        
        
        let mapLocationMoving = Observable.from([
            geoSearch,
            textSearch
        ])
            .merge()
            .asDriver(onErrorJustReturn: ApiController.Weather.dummy)
        
       
        
        mapLocationMoving.map {$0.coordinate} // mapLocationMoving가 결국은 weather 타입이라서 coordinate 사용해서 좌표로 변환.
        .drive(mapView.rx.centerCoordinate) // 맵뷰의 중앙을 맞춤
        .disposed(by: rx.disposeBag)
        
        let mapAround = mapInput.flatMap { (coor) in // 맵 중심좌표 가져다가
            return ApiController.shared.currentWeatherAround(lat: coor.latitude, lon: coor.longitude)
                .catchErrorJustReturn([])
        }.asDriver(onErrorJustReturn: [])
        
        mapAround.map { (weathers) in // 맵 주변 좌표를 가져서 맵뷰의 오버레이에 그린다.
            return weathers.map({ (weather) in
                return weather.overlay()
            })
        }
        .drive(mapView.rx.overlays)
        .disposed(by: rx.disposeBag)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        Appearance.applyBottomLine(to: searchCityName)
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    // MARK: - Style
    
    private func style() {
        // color 부분에서 Colors에 모아두는 것이 인상깊음
        view.backgroundColor = UIColor.aztec
        searchCityName.textColor = UIColor.ufoGreen
        tempLabel.textColor = UIColor.cream
        humidityLabel.textColor = UIColor.cream
        iconLabel.textColor = UIColor.cream
        cityNameLabel.textColor = UIColor.cream
    }
}

extension ViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    }
}

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? ApiController.Weather.Overlay {
            let overlayView = ApiController.Weather.OverlayView(overlay: overlay, overlayIcon: overlay.icon)
            return overlayView
        }
        return MKOverlayRenderer()
    }
}

주석으로 다 정리해 두었으니 살펴보기.

 

 

✅ [Color Set]

import Foundation
import UIKit

public func colorFromDecimalRGB(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, alpha: CGFloat = 1.0) -> UIColor {
  return UIColor(
    red: red / 255.0,
    green: green / 255.0,
    blue: blue / 255.0,
    alpha: alpha
  )
}

extension UIColor {

  // MARK: Custom Defined Colors

  /// Dark Blue Aztec
  class var aztec: UIColor {
    return colorFromDecimalRGB(38, 39, 41)
  }

  /// Light Cream Color
  class var lightCream: UIColor {
    return colorFromDecimalRGB(232, 234, 221)
  }

  /// Cream Color
  class var cream: UIColor {
    return colorFromDecimalRGB(229, 231, 218)
  }

  /// Swirl Color
  class var swirl: UIColor {
    return colorFromDecimalRGB(228, 221, 202)
  }

  /// Travertine Color
  class var travertine: UIColor {
    return colorFromDecimalRGB(214, 206, 195)
  }

  /// Green
  class var ufoGreen: UIColor {
    return colorFromDecimalRGB(64, 186, 145)
  }

  class var textGrey: UIColor {
    return colorFromDecimalRGB(146, 146, 146)
  }

}

Color Set을 다음과 같이 만들어서 사용하는 코드를 보았는데, 상당히 신선하고 다크모드와 같이 변경해주어야할 때, 상당히 신선하고 좋은 방법이라고 생각된다. 

더 좋은 방법은 Asset의 ColorAsset을 사용하면 다크모드를 조금 더 쉽게 지원할 수 있다.

 

 

✅ 밑줄 긋는 코드

import Foundation
import UIKit

public struct Appearance {

  // MARK: Component Theming
  static func applyBottomLine(to view: UIView, color: UIColor = UIColor.ufoGreen) {
    let line = UIView(frame: CGRect(x: 0, y: view.frame.height - 1, width: view.frame.width, height: 1))
    line.backgroundColor = color
    view.addSubview(line)
  }

}

예전에 어떤 컴포넌트에 밑줄을 그을 때, 각 컴포넌트 별로 다 그어주다 보니까 상당히 시간도 오래걸리고 반복되는 작업이었는데, 이 방법으로 작업하면 쉽게 작업할 수 있을 것 같아서 좋다.

 

✅ 적용하는 예시

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        Appearance.applyBottomLine(to: searchCityName)
    }

 

 

✅ imageFromText

fileprivate func imageFromText(text: NSString, font: UIFont) -> UIImage {
    
    let size = text.size(withAttributes: [NSAttributedString.Key.font: font])
    
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    text.draw(at: CGPoint(x: 0, y:0), withAttributes: [NSAttributedString.Key.font: font])
    
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return image ?? UIImage()
}

UILabel에 image를 주어 사용하는 방법도 있다.

 

✅ 🔥 가장 중요하다고 느끼는 API 처리하는 로직🔥

   개인적으로 API 부분을 rx로 처리하는게 상당히 어려운데 이걸 함께 보도록 하자.

import Foundation
import RxSwift
import RxCocoa
import SwiftyJSON
import CoreLocation
import MapKit

class ApiController {
    
    /// The shared instance
    static var shared = ApiController()
    
    /// The api key to communicate with openweathermap.org
    /// Create you own on https://home.openweathermap.org/users/sign_up
    private let apiKey = "5b0afdc348787659e01e0b959f8d9175"
    
    /// API base URL
    let baseURL = URL(string: "http://api.openweathermap.org/data/2.5")!
    
    init() {
        Logging.URLRequests = { request in
            return true
        }
    }
    
    //MARK: - Api Calls
    
    func currentWeather(city: String) -> Observable<Weather> {
        
        return buildRequest(pathComponent: "weather", params: [("q", city)]).map() { json in // map의 결과가 json임
            return Weather(
                cityName: json["name"].string ?? "N/A",
                temperature: json["main"]["temp"].int ?? 0,
                humidity: json["main"]["humidity"].int  ?? 0,
                icon: iconNameToChar(icon: json["weather"][0]["icon"].string ?? "e"),
                lat: json["coord"]["lat"].double ?? 0,
                lon: json["coord"]["lon"].double ?? 0
            )
        }
    }
    
    func currentWeather(lat: Double, lon: Double) -> Observable<Weather> {
        return buildRequest(pathComponent: "weather", params: [("lat", "\(lat)"), ("lon", "\(lon)")]).map() { json in
            return Weather(
                cityName: json["name"].string ?? "N/A",
                temperature: json["main"]["temp"].int ?? 0,
                humidity: json["main"]["humidity"].int  ?? 0,
                icon: iconNameToChar(icon: json["weather"][0]["icon"].string ?? "e"),
                lat: json["coord"]["lat"].double ?? 0,
                lon: json["coord"]["lon"].double ?? 0
            )
        }
    }
    
    func currentWeatherAround(lat: Double, lon: Double) -> Observable<[Weather]> {
        var weathers = [Observable<Weather>]()
        for i in -1...1 {
            for j in -1...1 {
                weathers.append(currentWeather(lat: lat + Double(i), lon: lon + Double(j)))
            }
        }
        return Observable.from(weathers).merge().toArray()
    }
    
    //MARK: - Private Methods
    
    /**
     * Private method to build a request with RxCocoa
     */
    private func buildRequest(method: String = "GET", pathComponent: String, params: [(String, String)]) -> Observable<JSON> {
        let url = baseURL.appendingPathComponent(pathComponent)
        var request = URLRequest(url: url)
        let keyQueryItem = URLQueryItem(name: "appid", value: apiKey)
        let unitsQueryItem = URLQueryItem(name: "units", value: "metric")
        let urlComponents = NSURLComponents(url: url, resolvingAgainstBaseURL: true)!
        
        if method == "GET" {
            var queryItems = params.map { URLQueryItem(name: $0.0, value: $0.1) }
            queryItems.append(keyQueryItem)
            queryItems.append(unitsQueryItem)
            urlComponents.queryItems = queryItems
        } else {
            urlComponents.queryItems = [keyQueryItem, unitsQueryItem]
            
            let jsonData = try! JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
            request.httpBody = jsonData
        }
        
        request.url = urlComponents.url!
        request.httpMethod = method
        
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let session = URLSession.shared
        
        return session.rx.data(request: request).map { try JSON(data: $0) }
    }
    
    /**
     * Weather information and map overlay
     */
    
    struct Weather {
        let cityName: String
        let temperature: Int
        let humidity: Int
        let icon: String
        let lat: Double
        let lon: Double
        
        static let empty = Weather(
            cityName: "N/A",
            temperature: 0,
            humidity: 0,
            icon: iconNameToChar(icon: "e"),
            lat: 0,
            lon: 0
        )
        
        static let dummy = Weather(
            cityName: "N/A",
            temperature: 0,
            humidity: 0,
            icon: iconNameToChar(icon: "e"),
            lat: 0,
            lon: 0
        )
        
        var coordinate: CLLocationCoordinate2D {
            return CLLocationCoordinate2D(latitude: lat, longitude: lon)
        }
        
        func overlay() -> Overlay {
            let coordinates: [CLLocationCoordinate2D] = [
                CLLocationCoordinate2D(latitude: lat - 0.25, longitude: lon - 0.25),
                CLLocationCoordinate2D(latitude: lat + 0.25, longitude: lon + 0.25)
            ]
            let points = coordinates.map { MKMapPoint($0) }
            let rects = points.map { MKMapRect(origin: $0, size: MKMapSize(width: 0, height: 0)) }
            let fittingRect = rects.reduce(MKMapRect.null) { (result, rect) in
                return result.union(rect)
            }
            return Overlay(icon: icon, coordinate: coordinate, boundingMapRect: fittingRect)
        }
        
        public class Overlay: NSObject, MKOverlay {
            var coordinate: CLLocationCoordinate2D
            var boundingMapRect: MKMapRect
            let icon: String
            
            init(icon: String, coordinate: CLLocationCoordinate2D, boundingMapRect: MKMapRect) {
                self.coordinate = coordinate
                self.boundingMapRect = boundingMapRect
                self.icon = icon
            }
        }
        
        public class OverlayView: MKOverlayRenderer {
            var overlayIcon: String
            
            init(overlay:MKOverlay, overlayIcon:String) {
                self.overlayIcon = overlayIcon
                super.init(overlay: overlay)
            }
            
            public override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
                let imageReference = imageFromText(text: overlayIcon as NSString, font: UIFont(name: "Flaticon", size: 32.0)!).cgImage
                let theMapRect = overlay.boundingMapRect
                let theRect = rect(for: theMapRect)
                
                context.scaleBy(x: 1.0, y: -1.0)
                context.translateBy(x: 0.0, y: -theRect.size.height)
                context.draw(imageReference!, in: theRect)
            }
        }
    }
}

/**
 * Maps an icon information from the API to a local char
 * Source: http://openweathermap.org/weather-conditions
 */
public func iconNameToChar(icon: String) -> String {
    switch icon {
    case "01d":
        return "\u{f11b}"
    case "01n":
        return "\u{f110}"
    case "02d":
        return "\u{f112}"
    case "02n":
        return "\u{f104}"
    case "03d", "03n":
        return "\u{f111}"
    case "04d", "04n":
        return "\u{f111}"
    case "09d", "09n":
        return "\u{f116}"
    case "10d", "10n":
        return "\u{f113}"
    case "11d", "11n":
        return "\u{f10d}"
    case "13d", "13n":
        return "\u{f119}"
    case "50d", "50n":
        return "\u{f10e}"
    default:
        return "E"
    }
}

fileprivate func imageFromText(text: NSString, font: UIFont) -> UIImage {
    
    let size = text.size(withAttributes: [NSAttributedString.Key.font: font])
    
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    text.draw(at: CGPoint(x: 0, y:0), withAttributes: [NSAttributedString.Key.font: font])
    
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return image ?? UIImage()
}

다른것보다도 가장 눈에 띄는건 우선 데이터를 request하고 response를 받았을 때, 데이터가 Observable은 아닐거 아니야? rx를 사용ㅇ하면 session 부분에 rx.data를 통해서 데이터를 JSON형태로 옵저버블로 내려보내게 돼. 이게 buildRequest 함수 부분이야. 그리고 Decoding은 주로 다른 쪽에서 하게되는데, 빌드 리퀘스트로 JSON 형태로 데이터를 받아와서 Weather에서 데이터를 가공 처리하게 된다.