쪽지발송 성공
Click here
재능넷 이용방법
재능넷 이용방법 동영상편
가입인사 이벤트
판매 수수료 안내
안전거래 TIP
재능인 인증서 발급안내

🌲 지식인의 숲 🌲

🌳 디자인
🌳 음악/영상
🌳 문서작성
🌳 번역/외국어
🌳 프로그램개발
🌳 마케팅/비즈니스
🌳 생활서비스
🌳 철학
🌳 과학
🌳 수학
🌳 역사
해당 지식과 관련있는 인기재능

애플리케이션 서비스 안녕하세요. 안드로이드 개발자입니다.여러분들의 홈페이지,블로그,카페,모바일 등 손쉽게 어플로 제작 해드립니다.요즘...

안녕하세요.2011년 개업하였고, 2013년 벤처 인증 받은 어플 개발 전문 업체입니다.50만 다운로드가 넘는 앱 2개를 직접 개발/운영 중이며,누구보...

IOS/Android/Win64/32(MFC)/MacOS 어플 제작해드립니다.제공된 앱의 화면은 아이폰,아이패드,안드로이드 모두  정확하게 일치합니...

안녕하세요.신호처리를 전공한 개발자 입니다. 1. 영상신호처리, 생체신호처리 알고리즘 개발2. 안드로이드 앱 개발 3. 윈도우 프로그램...

Swift에서 함수형 반응형 프로그래밍(FRP) 구현

2024-10-01 16:21:39

재능넷
조회수 256 댓글수 0

Swift에서 함수형 반응형 프로그래밍(FRP) 구현하기 🚀

 

 

안녕, 친구들! 오늘은 정말 흥미진진한 주제로 찾아왔어. 바로 Swift에서 함수형 반응형 프로그래밍(FRP)을 구현하는 방법에 대해 얘기해볼 거야. 😎 이 주제가 좀 어렵게 들릴 수도 있겠지만, 걱정 마! 내가 최대한 쉽고 재밌게 설명해줄게. 마치 우리가 커피숍에서 수다 떠는 것처럼 편하게 들어봐.

그리고 말이야, 이런 멋진 프로그래밍 기술을 배우다 보면 어느새 넌 개발 고수가 되어 있을 거야. 그때 네 실력을 뽐내고 싶다면 어떨까? 바로 재능넷(https://www.jaenung.net)이라는 재능 공유 플랫폼을 활용해보는 거야. 거기서 네 Swift 프로그래밍 실력을 다른 사람들과 나누고, 또 새로운 것도 배울 수 있을 거야. 멋지지 않아? 😉

자, 이제 본격적으로 시작해볼까? FRP라는 게 대체 뭐길래 이렇게 핫한 주제인 걸까? 함께 알아보자고!

FRP, 그게 뭐야? 🤔

FRP, 즉 함수형 반응형 프로그래밍. 이름부터가 좀 거창해 보이지? 하지만 걱정 마, 이것도 결국은 우리가 일상에서 경험하는 것들과 비슷해. 예를 들어볼게.

상상해봐. 넌 지금 스마트폰으로 음악을 듣고 있어. 갑자기 전화가 왔어. 뭐가 일어나? 음악이 자동으로 멈추지? 그리고 전화를 끊으면 다시 음악이 재생되고. 이게 바로 FRP의 기본 개념이야!

FRP는 데이터의 흐름과 변화를 자동으로 전파하는 프로그래밍 패러다임이야. 쉽게 말해, 어떤 값이 변하면 그 값을 사용하는 모든 곳에 자동으로 그 변화가 전달되는 거지. 마치 도미노처럼 말이야!

그럼 이걸 왜 배워야 할까? 🧐

  • 코드가 더 깔끔해져 (가독성 Up! ⬆️)
  • 버그가 줄어들어 (안정성 Up! ⬆️)
  • 비동기 작업을 다루기 쉬워져 (생산성 Up! ⬆️)

특히 Swift로 iOS 앱을 만들 때 FRP를 사용하면, 복잡한 UI 업데이트나 네트워크 요청 같은 작업을 훨씬 우아하게 처리할 수 있어. 멋지지 않아?

FRP 개념도 FRP 데이터 입력 자동 전파 UI 업데이트 상태 변화

이 그림을 보면 FRP의 핵심 개념을 한눈에 이해할 수 있어. 데이터가 입력되면, 그 변화가 자동으로 전파되어 UI를 업데이트하고, 앱의 상태를 변경하는 거지. 모든 게 유기적으로 연결되어 있어서 정말 효율적이야!

자, 이제 FRP가 뭔지 대충 감이 왔지? 그럼 이제 Swift에서 어떻게 이걸 구현하는지 하나씩 알아볼까? 준비됐어? Let's dive in! 🏊‍♂️

Swift에서 FRP 구현하기: 기초부터 시작! 🏗️

자, 이제 본격적으로 Swift에서 FRP를 어떻게 구현하는지 알아볼 거야. 하지만 걱정 마, 우리는 기초부터 차근차근 시작할 거니까!

1. 옵저버블(Observable) 만들기 👀

FRP의 핵심은 '옵저버블'이야. 옵저버블은 시간이 지남에 따라 변할 수 있는 값의 시퀀스를 나타내. 마치 유튜브 구독 같은 거지. 새 영상이 올라오면 알림을 받는 것처럼, 옵저버블의 값이 변하면 그걸 구독하고 있는 모든 곳에 알림이 가는 거야.

Swift에서 간단한 옵저버블을 만들어볼까?


class Observable<t> {
    private var value: T
    private var observers: [(T) -> Void] = []
    
    init(_ value: T) {
        self.value = value
    }
    
    var currentValue: T {
        get { return value }
        set {
            value = newValue
            notifyObservers()
        }
    }
    
    func bind(observer: @escaping (T) -> Void) {
        observers.append(observer)
        observer(value)
    }
    
    private func notifyObservers() {
        observers.forEach { $0(value) }
    }
}
</t>

우와, 코드가 좀 길어 보이지? 하지만 천천히 살펴보면 그렇게 복잡하지 않아. 하나씩 설명해줄게.

  • T: 제네릭 타입이야. 어떤 타입의 값이든 다룰 수 있게 해주지.
  • value: 우리가 관찰하고 싶은 실제 값이야.
  • observers: 이 값의 변화를 구독하고 있는 모든 관찰자들의 목록이야.
  • currentValue: 값을 읽거나 설정할 수 있게 해주는 프로퍼티야. 값이 바뀌면 자동으로 모든 관찰자에게 알려줘.
  • bind: 새로운 관찰자를 추가하는 메서드야.
  • notifyObservers: 모든 관찰자에게 값이 변경됐다고 알려주는 메서드야.

이제 이 Observable을 어떻게 사용하는지 볼까?


let nameObservable = Observable("Swift")

nameObservable.bind { newName in
    print("이름이 변경됐어요: \(newName)")
}

nameObservable.currentValue = "SwiftUI"
// 출력: 이름이 변경됐어요: SwiftUI

봐, 엄청 간단하지? nameObservable의 값이 바뀌면 자동으로 우리가 정의한 클로저가 실행돼. 이게 바로 FRP의 마법이야! 🎩✨

🚀 실전 팁: 이런 식으로 Observable을 사용하면 UI 업데이트를 정말 쉽게 할 수 있어. 예를 들어, 사용자의 이름을 표시하는 레이블이 있다고 해보자. 이 레이블의 텍스트를 nameObservable에 바인딩하면, 이름이 변경될 때마다 자동으로 UI가 업데이트될 거야!

2. 연산자(Operator) 추가하기 🧮

FRP의 또 다른 강점은 연산자를 통해 데이터 스트림을 변형하고 조작할 수 있다는 거야. Swift에서도 이런 연산자들을 구현할 수 있어. 간단한 예로 'map' 연산자를 만들어볼까?


extension Observable {
    func map<u>(_ transform: @escaping (T) -> U) -> Observable<u> {
        let newObservable = Observable<u>(transform(value))
        
        self.bind { [weak newObservable] value in
            newObservable?.currentValue = transform(value)
        }
        
        return newObservable
    }
}
</u></u></u>

이 'map' 연산자는 기존 Observable의 값을 변형해서 새로운 Observable을 만들어내. 어떻게 사용하는지 볼까?


let numberObservable = Observable(10)
let doubledObservable = numberObservable.map { $0 * 2 }

doubledObservable.bind { doubledValue in
    print("두 배로 증가한 값: \(doubledValue)")
}

numberObservable.currentValue = 20
// 출력: 두 배로 증가한 값: 40

와우! numberObservable의 값을 변경했는데, doubledObservable도 자동으로 업데이트됐어. 이게 바로 FRP의 매력이지. 데이터의 흐름을 정의하고 나면, 나머지는 알아서 처리되니까 얼마나 편해?

FRP 데이터 흐름 numberObservable 10 → 20 map x * 2 doubledObservable 20 → 40

이 그림을 보면 데이터가 어떻게 흘러가는지 한눈에 볼 수 있지? numberObservable의 값이 변하면, 그 값이 map 연산자를 통과해서 doubledObservable로 전달돼. 모든 과정이 자동으로 일어나니까 정말 편리해!

3. 에러 처리하기 🚨

FRP에서 중요한 또 다른 부분은 에러 처리야. 데이터 스트림에서 에러가 발생할 수 있으니까, 이를 우아하게 처리할 수 있어야 해. 우리의 Observable을 조금 수정해서 에러 처리 기능을 추가해볼까?


enum ObservableError: Error {
    case unknown
}

class Observable<t> {
    private var value: T
    private var observers: [(Result<t error>) -> Void] = []
    
    init(_ value: T) {
        self.value = value
    }
    
    var currentValue: T {
        get { return value }
        set {
            value = newValue
            notifyObservers(.success(value))
        }
    }
    
    func bind(observer: @escaping (Result<t error>) -> Void) {
        observers.append(observer)
        observer(.success(value))
    }
    
    func notifyError(_ error: Error) {
        observers.forEach { $0(.failure(error)) }
    }
    
    private func notifyObservers(_ result: Result<t error>) {
        observers.forEach { $0(result) }
    }
}
</t></t></t></t>

이제 Observable이 에러도 처리할 수 있게 됐어. 어떻게 사용하는지 볼까?


let ageObservable = Observable(20)

ageObservable.bind { result in
    switch result {
    case .success(let age):
        print("현재 나이: \(age)")
    case .failure(let error):
        print("에러 발생: \(error)")
    }
}

ageObservable.currentValue = 21
// 출력: 현재 나이: 21

ageObservable.notifyError(ObservableError.unknown)
// 출력: 에러 발생: unknown

이렇게 하면 데이터 스트림에서 발생할 수 있는 에러도 깔끔하게 처리할 수 있어. 실제 앱을 만들 때 이런 에러 처리는 정말 중요하지. 네트워크 요청이 실패하거나, 데이터 파싱에 문제가 생겼을 때 유용하게 쓸 수 있을 거야.

💡 Pro Tip: 실제 프로젝트에서는 RxSwift나 Combine 같은 라이브러리를 사용하는 경우가 많아. 이런 라이브러리들은 우리가 지금 만든 것보다 훨씬 더 많은 기능을 제공하고, 성능도 최적화되어 있지. 하지만 기본 개념은 우리가 지금 배운 것과 같아. 이렇게 직접 구현해보면 내부 동작 원리를 이해하는 데 큰 도움이 될 거야!

자, 여기까지 Swift에서 FRP를 구현하는 기초적인 방법을 알아봤어. 어때, 생각보다 어렵지 않지? 이제 이 개념들을 바탕으로 더 복잡한 기능들을 구현할 수 있을 거야. 다음 섹션에서는 좀 더 실전적인 예제를 통해 FRP의 강력함을 직접 체험해볼 거야. 준비됐어? Let's go! 🚀

실전 예제: 날씨 앱 만들기 ☀️🌧️

자, 이제 우리가 배운 FRP 개념을 실제 앱에 적용해볼 시간이야! 간단한 날씨 앱을 만들어볼 건데, 이 과정에서 FRP가 얼마나 유용한지 직접 체험해볼 수 있을 거야.

1. 모델 정의하기 📊

먼저 우리 앱에서 사용할 날씨 정보 모델을 정의해볼게.


struct Weather {
    let temperature: Double
    let condition: String
}

class WeatherViewModel {
    let weatherObservable = Observable<weather>(nil)
    
    func fetchWeather(for city: String) {
        // 여기서 실제로는 네트워크 요청을 보내겠지만, 예제를 위해 더미 데이터를 사용할게.
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            let weather = Weather(temperature: 25.5, condition: "맑음")
            self?.weatherObservable.currentValue = weather
        }
    }
}
</weather>

여기서 WeatherViewModel은 weatherObservable이라는 Observable 객체를 가지고 있어. 이 객체는 Weather 타입의 옵셔널 값을 갖고 있지. 왜 옵셔널일까? 처음에는 날씨 정보가 없을 수 있으니까!

2. 뷰 컨트롤러 구현하기 🖼️

이제 이 뷰모델을 사용해서 실제 UI를 업데이트하는 뷰 컨트롤러를 만들어볼게.


class WeatherViewController: UIViewController {
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var conditionLabel: UILabel!
    @IBOutlet weak var cityTextField: UITextField!
    
    let viewModel = WeatherViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bindViewModel()
    }
    
    func bindViewModel() {
        viewModel.weatherObservable.bind { [weak self] result in
            switch result {
            case .success(let weather):
                if let weather = weather {
                    DispatchQueue.main.async {
                        self?.temperatureLabel.text = "\(weather.temperature)°C"
                        self?.conditionLabel.text = weather.condition
                    }
                }
            case .failure(let error):
                print("날씨 정보를 가져오는데 실패했어요: \(error)")
            }
        }
    }
    
    @IBAction func fetchWeatherTapped(_ sender: Any) {
        guard let city = cityTextField.text, !city.isEmpty else {
            print("도시 이름을 입력해주세요!")
            return
        }
        
        viewModel.fetchWeather(for: city)
    }
}

와우, 이제 정말 FRP의 힘을 느낄 수 있지? bindViewModel() 메서드를 보면, weatherObservable에 바인딩을 하고 있어. 이렇게 하면 날씨 정보가 업데이트될 때마다 자동으로 UI가 갱신돼. 얼마나 편해?

날씨 앱 데이터 흐름 사용자 입력 WeatherViewModel weatherObservable UI 업데이트

이 다이어그램을 보면 데이터가 어떻게 흐르는지 한눈에 볼 수 있어. 사용자 입력이 WeatherViewModel로 전달되고, 그 결과가 weatherObservable을 통해 UI로 전달되는 거지. 모든 과정이 자동으로 일어나니까 정말 편리하지?

3. 더 많은 기능 추가하기 🚀

이제 기본적인 구조가 만들어졌으니, 여기에 더 많은 FRP 기능들을 추가해볼 수 있어. 예를 들어, 도시 이름 입력에 debounce를 적용하거나, 최근 검색한 도시 목록을 Observable로 관리하는 등의 기능을 추가할 수 있지.


class WeatherViewModel {
    let weatherObservable = Observable<weather>(nil)
    let recentSearches = Observable<[String]>([])
    
    private var searchTask: DispatchWorkItem?
    
    func searchWeather(for city: String) {
        searchTask?.cancel()
        
        let task = DispatchWorkItem { [weak self] in
            self?.fetchWeather(for: city)
            self?.ad  dRecentSearch(city)
        }
        
        searchTask = task
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task)
    }
    
    private func addRecentSearch(_ city: String) {
        var searches = recentSearches.currentValue
        searches.insert(city, at: 0)
        if searches.count > 5 {
            searches.removeLast()
        }
        recentSearches.currentValue = searches
    }
    
    func fetchWeather(for city: String) {
        // 실제 네트워크 요청 대신 더미 데이터 사용
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            let weather = Weather(temperature: Double.random(in: 0...35), condition: ["맑음", "흐림", "비", "눈"].randomElement()!)
            self?.weatherObservable.currentValue = weather
        }
    }
}
</weather>

이렇게 하면 사용자가 도시 이름을 입력할 때마다 즉시 검색하지 않고, 0.5초 동안 기다렸다가 검색을 수행해. 이런 방식을 'debounce'라고 하는데, 불필요한 네트워크 요청을 줄이는 데 아주 유용해.

또한, 최근 검색한 도시 목록을 Observable로 관리하고 있어. 이를 통해 UI에서 최근 검색 목록을 쉽게 표시하고 업데이트할 수 있지.

4. UI 업데이트하기 🖼️

이제 이 새로운 기능들을 뷰 컨트롤러에 적용해볼게.


class WeatherViewController: UIViewController {
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var conditionLabel: UILabel!
    @IBOutlet weak var cityTextField: UITextField!
    @IBOutlet weak var recentSearchesTableView: UITableView!
    
    let viewModel = WeatherViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bindViewModel()
        setupTextField()
    }
    
    func bindViewModel() {
        viewModel.weatherObservable.bind { [weak self] result in
            switch result {
            case .success(let weather):
                if let weather = weather {
                    DispatchQueue.main.async {
                        self?.temperatureLabel.text = "\(weather.temperature)°C"
                        self?.conditionLabel.text = weather.condition
                    }
                }
            case .failure(let error):
                print("날씨 정보를 가져오는데 실패했어요: \(error)")
            }
        }
        
        viewModel.recentSearches.bind { [weak self] searches in
            DispatchQueue.main.async {
                self?.recentSearchesTableView.reloadData()
            }
        }
    }
    
    func setupTextField() {
        cityTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }
    
    @objc func textFieldDidChange(_ textField: UITextField) {
        guard let city = textField.text else { return }
        viewModel.searchWeather(for: city)
    }
}

extension WeatherViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.recentSearches.currentValue.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "RecentSearchCell", for: indexPath)
        cell.textLabel?.text = viewModel.recentSearches.currentValue[indexPath.row]
        return cell
    }
}

와우! 이제 우리 앱이 정말 반응형이 됐어. 사용자가 도시 이름을 입력하면 자동으로 날씨 정보를 가져오고, 최근 검색 목록도 자동으로 업데이트돼. 모든 것이 Observable을 통해 연결되어 있어서 데이터의 흐름이 정말 깔끔해졌지?

🌟 FRP의 장점: 1. 코드의 가독성이 높아져요. 데이터의 흐름을 한눈에 볼 수 있거든요. 2. 비동기 작업을 다루기가 훨씬 쉬워져요. 콜백 지옥에서 벗어날 수 있죠! 3. 상태 관리가 간편해져요. 모든 상태 변화가 Observable을 통해 전파되니까요. 4. 테스트하기 쉬워져요. 데이터 흐름을 쉽게 모킹하고 테스트할 수 있어요.

자, 여기까지 Swift에서 FRP를 구현하고 실제 앱에 적용하는 방법을 알아봤어. 어때, 생각보다 어렵지 않지? FRP를 사용하면 복잡한 비동기 작업도 우아하게 처리할 수 있고, UI 업데이트도 정말 간편해져.

물론 이건 FRP의 아주 기초적인 부분일 뿐이야. 실제로는 RxSwift나 Combine 같은 라이브러리를 사용하면 더 많은 기능을 활용할 수 있어. 하지만 이렇게 직접 구현해보면 FRP의 핵심 개념을 정말 잘 이해할 수 있지.

앞으로 더 복잡한 앱을 만들 때 FRP를 활용해보면, 코드가 얼마나 깔끔해지고 유지보수가 쉬워지는지 직접 경험할 수 있을 거야. 그리고 이런 실력을 쌓다 보면 재능넷(https://www.jaenung.net)에서 네 실력을 뽐내고 다른 개발자들과 지식을 나눌 수 있을 거야. 멋지지 않아?

자, 이제 네가 배운 걸 활용해서 더 멋진 앱을 만들어볼 차례야. 화이팅! 🚀

관련 키워드

  • Swift
  • FRP
  • 함수형 반응형 프로그래밍
  • Observable
  • 옵저버블
  • 연산자
  • 에러 처리
  • 비동기 프로그래밍
  • UI 업데이트
  • 데이터 바인딩

지식의 가치와 지적 재산권 보호

자유 결제 서비스

'지식인의 숲'은 "이용자 자유 결제 서비스"를 통해 지식의 가치를 공유합니다. 콘텐츠를 경험하신 후, 아래 안내에 따라 자유롭게 결제해 주세요.

자유 결제 : 국민은행 420401-04-167940 (주)재능넷
결제금액: 귀하가 받은 가치만큼 자유롭게 결정해 주세요
결제기간: 기한 없이 언제든 편한 시기에 결제 가능합니다

지적 재산권 보호 고지

  1. 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
  2. AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
  3. 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
  4. 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
  5. AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.

© 2024 재능넷 | All rights reserved.

댓글 작성
0/2000

댓글 0개

해당 지식과 관련있는 인기재능

 안녕하세요 현재 안드로이드 기반 어플리케이션 제작 및 서비스를 하고 있으며,스타트업회사에 재직중입니다.- 개인앱, 프로젝트용 앱 등부...

------------------------------------만들고 싶어하는 앱을 제작해드립니다.------------------------------------1. 안드로이드 ( 자바 )* 블루...

소개안드로이드 기반 어플리케이션 개발 후 서비스를 하고 있으며 스타트업 경험을 통한 앱 및 서버, 관리자 페이지 개발 경험을 가지고 있습니다....

 주문전 꼭 쪽지로 문의메세지 주시면 감사하겠습니다.* Skills (order by experience desc)Platform : Android, Web, Hybrid(Cordova), Wind...

📚 생성된 총 지식 7,558 개

  • (주)재능넷 | 대표 : 강정수 | 경기도 수원시 영통구 봉영로 1612, 7층 710-09 호 (영통동) | 사업자등록번호 : 131-86-65451
    통신판매업신고 : 2018-수원영통-0307 | 직업정보제공사업 신고번호 : 중부청 2013-4호 | jaenung@jaenung.net

    (주)재능넷의 사전 서면 동의 없이 재능넷사이트의 일체의 정보, 콘텐츠 및 UI등을 상업적 목적으로 전재, 전송, 스크래핑 등 무단 사용할 수 없습니다.
    (주)재능넷은 통신판매중개자로서 재능넷의 거래당사자가 아니며, 판매자가 등록한 상품정보 및 거래에 대해 재능넷은 일체 책임을 지지 않습니다.

    Copyright © 2024 재능넷 Inc. All rights reserved.
ICT Innovation 대상
미래창조과학부장관 표창
서울특별시
공유기업 지정
한국데이터베이스진흥원
콘텐츠 제공서비스 품질인증
대한민국 중소 중견기업
혁신대상 중소기업청장상
인터넷에코어워드
일자리창출 분야 대상
웹어워드코리아
인터넷 서비스분야 우수상
정보통신산업진흥원장
정부유공 표창장
미래창조과학부
ICT지원사업 선정
기술혁신
벤처기업 확인
기술개발
기업부설 연구소 인정
마이크로소프트
BizsPark 스타트업
대한민국 미래경영대상
재능마켓 부문 수상
대한민국 중소기업인 대회
중소기업중앙회장 표창
국회 중소벤처기업위원회
위원장 표창