저번부터 한번쯤 만들어보고 싶었던 단축링크 서비스를 만들며 생각했던 내용과 플로우를 정리한 내용을 공개합니다. 서비스 개발 기간은 (2019.10.12 ~ 10.13) 이틀간 진행한 개인 토이 프로젝트로 전체 코드는 깃허브 ( https://github.com/sistinafibel/modu.link )에서 보실 수 있습니다.
일단 먼저 체험해보고 싶다면 이쪽 > modu.link
개요
나는 특이하게도 (고질병 같다) 불타게 주말을 보낼 정도 노오력으로 만들 수 있는 서비스라면 내 자체 서비스로 만들려고 하는 서비스들이 많다.
예시를 들면..
- SMS 휴대폰 인증 서비스 (만들었음 - 포스트랑 공개 예정)
- WOL 지원하지 않는 공유기를 위한 원격 부팅 (하드웨어 공수도 들어가서 따로 포스팅 예정)
- 단축링크 서비스 (Short Link ) < 오늘 공개할 서비스 개발기
이미 서비스중이거나 대기업에서 해주는 API 들도 있겠지만, 모 내가 서비스 만들면 관련 서비스에 대한 로그도 쌓이고 트래픽도 체크하고 매부좋고 누이좋은거 아니겠나. 오늘 공개할 서비스는 다른곳에서 제공하는 숏 링크의 URL이 썩 맘에 들지 않아서 더욱 더 만들어보고 싶었다.
단축 링크를 만들면 뭐가 좋은가요?
- 어어엄청 긴 링크도 짧아져서 이쁨
- 필요한 무수한 GET 파라미터가 보이질 않음.
단점은.. SMS로 보내면 되게 파싱 사이트로 갈 것 같아보임.
프로세스
단축 링크 서비스를 만들며 생각한 핵심 프로세스는 다음과 같다.
- 이용자가 긴 URL 링크를 입력한다.
- 입력한 URL을 서버에서는 데이터베이스에 저장한뒤, 랜덤으로 생성된 6자리의 대소문자+숫자가 포함된 단축 링크를 이용자에게 리턴해준다.
- 이용자는 받은 단축링크를 공유하거나 배포하여 다른 이용자가 단축링크를 클릭한다.
- 클릭한 단축링크는 다시 단축링크 서버로 들어가 매칭되는 원본URL 값을 찾은뒤 리다이렉트 시켜준다.
간단하다!
1) 기본 설정하기
사용한 데이터베이스는 mySql (5.7) 서버는 node10.16.3LTS(Express) 를 사용했고
기본적으로 데이터베이스 연결을 위한 .env 파일을 만들어 환경변수로 분기시켜줬다.
#.env
SERVER_URL=서비스할 도메인명
DB_HOST= 디비 아이피
DB_USER=디비 아이디
DB_PASSWORD=디비 패스워드
DB_PORT=디비 포트
DB_TABLE_NAME=디비 테이블명
이후 .env 파일을 읽기위한 dotenv도 설치하여 추가시켜줬다.
require('dotenv').config();
2) 서비스 만들기
메인페이지는 URL을 요청하고 받을 수 있도록 준비해두고 api 서비스를 만들기 시작했다.
(이 포스팅에서는 서비스 구현 부분에 대한 코드만 넣을 생각이니 FullCode는 깃허브를 참조)
도메인 접속시 GET 방식으로 index 페이지를 호출하도록 구성하였고, 동시에 env에서 설정한 SERVER_URL 값을 가져와서 페이지에 뿌려줬다.
mainController.js
/**
* GET
* 메인페이지
*/
router.get('/', function(req, res, next) {
res.render('index' , {serviceUrl : process.env.SERVER_URL});
});
뷰 페이지에서 단축 URL 생성
/**
* POST
* 단축 URL을 생성합니다.
*/
router.post('/addUrlGeneration', async function(req, res, next) {
let userUrl = encodeURI(req.body.userUrl);
let regex = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
if(!regex.test(userUrl)){ //URL 정규식 확인
resJson("600" , "URL주소가 아닙니다.");
}
let sqlValue = {};
//이용자 아이피 정보 받아오기
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
let retrun_url = makeid();
sqlValue = {
url : userUrl, //유저가 보낸 URL
urltype : req.body.sslSet, //http ? https
return_url : retrun_url, // 리턴결과값
domain : req.body.domainSet,
etcset : req.body.etcSet
};
console.log(sqlValue);
let urlInf = await mainDao.addUrlInf(sqlValue); //await 예외처리 필요..
console.log("------------------------------");
let jsonReturn = {
status : "200",
text : "정상적으로 생성되었습니다.",
url : userUrl,
urltype : req.body.sslSet,
return_url : retrun_url,
domain : req.body.domainSet,
etcset : req.body.etcSet,
serviceUrl : process.env.SERVER_URL
}
res.json(jsonReturn); //최종 결과 이용자에게 전달
});
/**
* res JSON 전용으로 오류 메세지용도로 공통화 처리
* @param {*} statusCode
* @param {*} text
*
*/
function resJson(statusCode , text){
let jsonReturn = {
status : statusCode,
text : text
}
res.json(jsonReturn);
return 0;
}
/**
* 7자리의 랜덤 String값을 전달합니다.
*/
function makeid()
{
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 7; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
쿼리단 분기 처리 mainDao.js // mainDao.addUrlInf
//새로운 URL 정보 추가
mainDao.addUrlInf = function(param){
return new Promise(function (resolve, reject){
let sql = "INSERT INTO urllist SET ? ";
let sqlValue = param;
pool.getConnection(function(err, connection) {
try{
connection.query(sql, sqlValue, function(err, result, fields) {
console.log("--------------------------------------");
if (err) {
console.log(err);
reject(err.code);
} else {
resolve(result);
}
})
}catch (e){
console.log(e);
}finally{
connection.release();
}
});
});
}
축소된 URL -> 실제 URL로 이동
/**
* GET
* 축소URL -> 할당된 URL로 이동
* 축소된 파라미터로 다른 페이지로 리다이렉트 시킵니다.
*/
router.get('/:url', async function(req, res, next) {
sqlArray = [ encodeURI(req.params.url) ];
let getUrlInf = await mainDao.getUrlInf(sqlArray);
if(commons.isEmpty(getUrlInf)){
res.render('black'); //오류 페이지 안내
return 0;
}
console.log(getUrlInf);
console.log(getUrlInf[0].url);
res.statusCode = 302; //리다이렉트를 위한 status코드
res.setHeader('Location', getUrlInf[0].url); //리다이렉트 경로
res.end();
});
mainDao.js // mainDao.getUrlInf
mainDao.getUrlInf = function(param){
return new Promise(function (resolve, reject){
let sql = "SELECT * FROM urllist WHERE 1=1 AND return_url = ?";
let sqlValue = param;
console.log(param);
pool.getConnection(function(err, connection) {
try{
connection.query(sql, sqlValue, function(err, result, fields) {
if (err) {
console.log(err);
reject(err.code);
} else {
console.log(result);
resolve (result);
}
});
}catch(e){
console.log(e);
}finally{
connection.release();
}
});
});
};
최종 결과
WoW!
마치며
만들면서 너무 재미있게 했다는 점이 가장 좋았다. 이 서비스를 실제로 도입을 하려고 생각해보면 아직 부족한 예외처리와 이용자 로그, 통계나 Qr코드 , API등 제공을 해야겠지만 짬짬히 시간 나는대로 개발해서 실제 서비스로 런칭해보고 싶은 서비스.
아쉬운 점.
FaaS와 NoSQL를 적용하기 딱 좋은 프로젝트였는데 일단 빨리빨리 만든다고 적용을 시키지 않은게 조금 아쉬움으로 다가온다. 빠른 시일내에 적용 시켜볼 생각!