저번부터 한번쯤 만들어보고 싶었던 단축링크 서비스를 만들며 생각했던 내용과 플로우를 정리한 내용을 공개합니다. 서비스 개발 기간은 (2019.10.12 ~ 10.13) 이틀간 진행한 개인 토이 프로젝트로 전체 코드는 깃허브 ( https://github.com/sistinafibel/modu.link )에서 보실 수 있습니다.

일단 먼저 체험해보고 싶다면 이쪽 > modu.link


개요

나는 특이하게도 (고질병 같다) 불타게 주말을 보낼 정도 노오력으로 만들 수 있는 서비스라면 내 자체 서비스로 만들려고 하는 서비스들이 많다.

예시를 들면..

이미 서비스중이거나 대기업에서 해주는 API 들도 있겠지만, 모 내가 서비스 만들면 관련 서비스에 대한 로그도 쌓이고 트래픽도 체크하고 매부좋고 누이좋은거 아니겠나. 오늘 공개할 서비스는 다른곳에서 제공하는 숏 링크의 URL이 썩 맘에 들지 않아서 더욱 더 만들어보고 싶었다.

단축 링크를 만들면 뭐가 좋은가요?

단점은.. SMS로 보내면 되게 파싱 사이트로 갈 것 같아보임.


프로세스

단축 링크 서비스를 만들며 생각한 핵심 프로세스는 다음과 같다.

  1. 이용자가 긴 URL 링크를 입력한다.
  2. 입력한 URL을 서버에서는 데이터베이스에 저장한뒤, 랜덤으로 생성된 6자리의 대소문자+숫자가 포함된 단축 링크를 이용자에게 리턴해준다.
  3. 이용자는 받은 단축링크를 공유하거나 배포하여 다른 이용자가 단축링크를 클릭한다.
  4. 클릭한 단축링크는 다시 단축링크 서버로 들어가 매칭되는 원본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();
            }
        });
    });
};

최종 결과

q111

WoW!

마치며

만들면서 너무 재미있게 했다는 점이 가장 좋았다. 이 서비스를 실제로 도입을 하려고 생각해보면 아직 부족한 예외처리와 이용자 로그, 통계나 Qr코드 , API등 제공을 해야겠지만 짬짬히 시간 나는대로 개발해서 실제 서비스로 런칭해보고 싶은 서비스.

아쉬운 점.

FaaS와 NoSQL를 적용하기 딱 좋은 프로젝트였는데 일단 빨리빨리 만든다고 적용을 시키지 않은게 조금 아쉬움으로 다가온다. 빠른 시일내에 적용 시켜볼 생각!