Xcode 9.2에서 swift언어를 사용해서 웹서버와 통신하는 아이폰 앱을 개발할 경우 node.js를 사용해서 RESTful한 서비스를 만들고 간단한 통신을 구현하는 데모입니다.
수업시간에 많이 받는 질문중에 하나 입니다. 내가 만든 아이폰 앱이 어떻게 웹서버와 통신해서 입출력을 할 수 있습니까? 흔히 RESTful한 API기반의 웹 페이지를 구축해서 통신을 하라고 말합니다. 간단한 예제를 살펴보려고 합니다.
그런데 생각보다 간단하지는 않습니다~~ 약간이라도 알아야할 사전 지식들이 있습니다. 일단 swift라는 언어를 알아야 합니다. 언어에 대한 내용을 조금씩 정리해서 올리려고 합니다. 웹의 경우 html태그와 서버측, 클라이언트측이라는 용어정도를 알아야 합니다. 일단은 무작정 따라하는 예제를 통해서 감을 잡을 수 있도록 진행해 보겠습니다.
몇가지 준비가 필요합니다. 일단 외부에 웹서버가 없다면 node.js를 사용해서 직접 웹서버를 구축하는 방법이 있습니다.
첫번째로 노드를 직접 맥에 설치하는 방법입니다.
맥에서 터미널을 오픈하고 아래와 같이 실행합니다. node.js를 먼저 설치해야 RESTful서비스를 테스트 할 수 있습니다. 맥에서 node.js를 설치하려면 먼저 brew가 설치되어 있어야 합니다. 그래서 brew를 먼저 설치하고 그리고 node를 설치합니다.
- 이미 node.js가 설치되어 있는데 node.js를 언인스톨하는 경우
brew uninstall node
- brew를 설치하는 방법
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
(brew를 언인스톨하는 방법)
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"
- node.js설치
brew install node
(혹시 업데이트하라고 뜨면 brew update node 명령을 실행합니다.)
맥의 바탕화면에 node라는 폴더를 만들고 여기에 웹서비스를 위한 파일들을 셋팅합니다.
data폴더 아래의 device.json은 아래와 같이 코딩합니다.
{
"iphone": {
"numbers": "5",
"name": "iphone"
},
"ipad": {
"numbers": "10",
"name": "ipad"
}
}
server.js는 아래와 같이 작성합니다.
module.exports = function(app, fs)
{
app.get('/',function(req,res){
res.render('index', {
title: "MY HOMEPAGE",
length: 5
})
});
app.get('/currentTime', function(req,res) {
res.send(new Date().toISOString());
});
app.get('/list', function (req, res) {
fs.readFile( __dirname + "/../data/" + "device.json", 'utf8', function (err, data) {
console.log( data );
res.end( data );
});
})
app.get('/getDevice/:devicename', function(req, res){
fs.readFile( __dirname + "/../data/device.json", 'utf8', function (err, data) {
var devices = JSON.parse(data);
res.json(devices[req.params.devicename]);
});
});
app.post('/addDevice/:devicename', function(req, res){
var result = { };
var devicename = req.params.devicename;
// CHECK REQ VALIDITY
if(!req.body["numbers"] || !req.body["name"]){
result["success"] = 0;
result["error"] = "invalid request";
res.json(result);
return;
}
// LOAD DATA & CHECK DUPLICATION
fs.readFile( __dirname + "/../data/device.json", 'utf8', function(err, data){
var devices = JSON.parse(data);
if(devices[devicename]){
// DUPLICATION FOUND
result["success"] = 0;
result["error"] = "duplicate";
res.json(result);
return;
}
// ADD TO DATA
devices[devicename] = req.body;
// SAVE DATA
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result = {"success": 1};
res.json(result);
})
})
});
app.put('/updateDevice/:devicename', function(req, res){
var result = { };
var devicename = req.params.devicename;
// CHECK REQ VALIDITY
if(!req.body["numbers"] || !req.body["name"]){
result["success"] = 0;
result["error"] = "invalid request";
res.json(result);
return;
}
// LOAD DATA
fs.readFile( __dirname + "/../data/device.json", 'utf8', function(err, data){
var devices = JSON.parse(data);
// ADD/MODIFY DATA
devices[devicename] = req.body;
// SAVE DATA
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result = {"success": 1};
res.json(result);
})
})
});
app.delete('/deleteDevice/:devicename', function(req, res){
var result = { };
//LOAD DATA
fs.readFile(__dirname + "/../data/device.json", "utf8", function(err, data){
var devices = JSON.parse(data);
// IF NOT FOUND
if(!devices[req.params.devicename]){
result["success"] = 0;
result["error"] = "not found";
res.json(result);
return;
}
// DELETE FROM DATA
delete devices[req.params.devicename];
// SAVE FILE
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result["success"] = 1;
res.json(result);
return;
})
})
})
}
server.js는 아래와 같이 코딩합니다.
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var session = require('express-session');
var fs = require("fs")
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);
var server = app.listen(3000, function(){
console.log("Express server has started on port 3000")
});
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(session({
secret: '@#$DemoSign$#',
resave: false,
saveUninitialized: true
}));
var router = require('./router/main')(app, fs);
route폴더 아래의 main.js는 아래와 같이 코딩합니다.
module.exports = function(app, fs)
{
app.get('/',function(req,res){
res.render('index', {
title: "MY HOMEPAGE",
length: 5
})
});
app.get('/currentTime', function(req,res) {
res.send(new Date().toISOString());
});
app.get('/list', function (req, res) {
fs.readFile( __dirname + "/../data/" + "device.json", 'utf8', function (err, data) {
console.log( data );
res.end( data );
});
})
app.get('/getDevice/:devicename', function(req, res){
fs.readFile( __dirname + "/../data/device.json", 'utf8', function (err, data) {
var devices = JSON.parse(data);
res.json(devices[req.params.devicename]);
});
});
app.post('/addDevice/:devicename', function(req, res){
var result = { };
var devicename = req.params.devicename;
// CHECK REQ VALIDITY
if(!req.body["numbers"] || !req.body["name"]){
result["success"] = 0;
result["error"] = "invalid request";
res.json(result);
return;
}
// LOAD DATA & CHECK DUPLICATION
fs.readFile( __dirname + "/../data/device.json", 'utf8', function(err, data){
var devices = JSON.parse(data);
if(devices[devicename]){
// DUPLICATION FOUND
result["success"] = 0;
result["error"] = "duplicate";
res.json(result);
return;
}
// ADD TO DATA
devices[devicename] = req.body;
// SAVE DATA
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result = {"success": 1};
res.json(result);
})
})
});
app.put('/updateDevice/:devicename', function(req, res){
var result = { };
var devicename = req.params.devicename;
// CHECK REQ VALIDITY
if(!req.body["numbers"] || !req.body["name"]){
result["success"] = 0;
result["error"] = "invalid request";
res.json(result);
return;
}
// LOAD DATA
fs.readFile( __dirname + "/../data/device.json", 'utf8', function(err, data){
var devices = JSON.parse(data);
// ADD/MODIFY DATA
devices[devicename] = req.body;
// SAVE DATA
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result = {"success": 1};
res.json(result);
})
})
});
app.delete('/deleteDevice/:devicename', function(req, res){
var result = { };
//LOAD DATA
fs.readFile(__dirname + "/../data/device.json", "utf8", function(err, data){
var devices = JSON.parse(data);
// IF NOT FOUND
if(!devices[req.params.devicename]){
result["success"] = 0;
result["error"] = "not found";
res.json(result);
return;
}
// DELETE FROM DATA
delete devices[req.params.devicename];
// SAVE FILE
fs.writeFile(__dirname + "/../data/device.json",
JSON.stringify(devices, null, '\t'), "utf8", function(err, data){
result["success"] = 1;
res.json(result);
return;
})
})
})
}
두번째는 외부 라이브러리를 사용하기 위해서 Pod설치하기 입니다. Pod를 설치하려면 아래와 같이 입력합니다.
sudo gem install cocoapods
약간의 부가적인 설명을 한다면 CocoaPods는 코코아 프로젝트에 대한 의존성 패키지를 관리하는 도구 중에 하나입니다. 최근에는 개발 생산성을 향상시키기 위한 목적으로 주요 라이브러리들을 데이터베이스화 해서 관리해 주는 도구들이 많이 생겨났습니다. 대표적인 패키지 관리 도구는 CentOS 리눅스에서 사용하는 yum, Fedora계열의 리눅스에서 사용하는 apt-get, 파이썬 패키지를 관리하는 pip, 루비 패키지를 관리하는 gem, node.js를 관리하는 npm등이 있습니다.
애플 진영에는
- 코코아팟(CocoaPods)
- 스위프트 패키지 관리자(SPM)
등이 있습니다.
코코아팟은 루비 언어를 이용해서 만들어진 패키지 의존성 관리 도구로써 지원하는 라이브러리들을 팟(Pod)이라는 개념으로 다룹니다. 코코아팟이라는 이름은 코코아 개발 환경 + 라이브러리들이라는 의미입니다.
세번째는 DemoAPITest란 이름으로 싱글뷰 기반의 앱을 생성합니다.
네번째는 통신에 대한 코드가 양이 많기 때문에 쉽게 처리하기 위해 Alamofire라이브러리를 설치합니다.
Alamofire 라이브러리를 Pod기반으로 설치하는 방법입니다. 아래의 내용을 복사해서 DemoAPITest폴더에 "Podfile"로 저장합니다.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!
target 'DemoAPITest' do
pod 'Alamofire', '~> 4.5'
end
위의 폴더에서 아래의 명령으로 스크립트를 실행합니다. 시간이 걸릴 수 있기 때문에 기다려야 합니다.
pod install
맥의 파인더로 보면 데스크탑에 생성된 DemoAPITest폴더 안에 workspace확장자로 된 파일을 더블 클릭해야 합니다.
Xcode 9.2에서 제공하는 Main.storyboard를 클릭해서 첫번째 뷰컨트롤러 위에 라벨 1개와 텍스트 필드 2개와 버튼 3개를 올려둡니다. 라벨은 아웃렛으로 currentTime으로 연결합니다. 텍스트필드는 각각 name, numbers로 연결한다. 버튼은 액션으로 각각 callCurrentTime, post, alamofire로 연결합니다.
DemoAPITest프로젝트의 ViewController.swift파일에 아래와 같이 코딩합니다.
//
// ViewController.swift
// DemoAPITest
//
// Created by papasmf on 2018. 3. 5..
// Copyright © 2018년 multicampus. All rights reserved.
//
import UIKit
import Alamofire
class ViewController: UIViewController {
@IBOutlet weak var currentTime: UILabel!
@IBOutlet weak var name: UITextField!
@IBOutlet weak var numbers: UITextField!
@IBOutlet weak var responseView: UITextView!
@IBAction func alamoreFire(_ sender: Any) {
let name = (self.name.text)!
let numbers = (self.numbers.text)!
let url = "http://localhost:3000/addDevice/" + "\(name)"
let param: Parameters = [
"name":name,
"numbers":numbers]
let alamo = Alamofire.request(url, method: .post,
parameters: param,
encoding: URLEncoding.httpBody)
alamo.responseJSON() {
response in
print("JSON=\(response.result.value!)")
if let jsonObject = response.result.value as? [String:Any] {
print("success = \(jsonObject["success"]!)")
}
}
}
@IBAction func post(_ sender: Any) {
let name = (self.name.text)!
let numbers = (self.numbers.text)!
let param = ["numbers":numbers, "name":name]
let paramData = try! JSONSerialization.data(withJSONObject: param, options: [])
let urlString = "http://localhost:3000/addDevice/" + "\(name)"
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.httpBody = paramData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(String(paramData.count), forHTTPHeaderField: "Content-Length")
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
if let e = error {
NSLog("An error has occured : \(e.localizedDescription)")
return
}
DispatchQueue.main.async() {
do {
let object = try JSONSerialization.jsonObject(with: data!, options: [])
as? NSDictionary
guard let jsonObject = object else { return }
let result = jsonObject["success"] as? Int
self.responseView.text = "입력 성공: \(result!)"
} catch let e as NSError {
print("error : \(e.localizedDescription)")
}
}
}
task.resume()
}
@IBAction func callCurrentTime(_ sender: Any) {
do {
let url = URL(string: "http://localhost:3000/currentTime")
let response = try String(contentsOf: url!)
self.currentTime.text = response
self.currentTime.sizeToFit()
} catch let e as NSError {
print(e.localizedDescription)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
node폴더에서 node server.js 로 실행을 하면 시간을 받아오거나 POST로 새로운 데이터를 입력할 수 있습니다. URLSession을 직접사용하는 것이 코딩량이 많기 때문에 Alamofire를 사용한 코드와 비교해서 살펴봅니다.