계속 공부만 하다가 프로젝트를 하고 싶어서 간단히 URL 단축 사이트를 만들어 봤습니다.
기술 스택:
Node.JS, MongoDB, HTML, CSS, Bootstrap
NPM 모듈:
Express, BodyParser, EJS, Mongoose, Nanoid, dotenv
(이 포스트에서는 자세하게 어떤 코드가 어떤 일을 하는지 설명하지 않습니다.)
완성된 결과는 여기에서 확인하세요!
***12/2월 업데이트 - 비밀번호 도입/복사***
***12/8 업데이트 - 복사 기능 업데이트***
사실 처음 생각은 이걸 누구나 Heroku(무료 호스팅 서비스) 같은 곳에 쉽게 호스팅 하게 하고 싶었는데
하다 보니까 (저한테는) 간단한 프로젝트가 아니게 돼서 그냥 제 개인용으로 만들었습니다.
작동원리는 그냥 간단하게 만들었습니다.
몽고디비 아틀라스랑 연결해서 클라우드로 데이터베이스를 연결하고 nanoid 모듈을 통해 단축 링크를 생성합니다.
몽고 디비 스키마는 단축 링크, 기존 링크, 클릭 횟수 세 가지로 구성되어 있습니다.
(사실 클릭 횟수는 필요 없는데 혹시라도 코드 쓰실 분 있으면 사용하세요)
// 몽고디비 스키마
const linkSchema = new mongoose.Schema({
identifier: {
required: true,
type: String,
},
url: {
required: true,
type: String,
},
clicks: {
required: true,
type: Number,
default: 0,
}
});
만약 새로운 단축 링크를 생성해야하면 nanoid를 통해 (알파벳, 숫자, -, _)들로 구성된 랜덤 한 7 문자 문자열이 생성됩니다.
만약 이 문자열이 데이터베이스에 등록되어있지 않으면 이 문자열, 기존 링크, 클릭 횟수(0)로 새로운 document를 디비에 추가합니다.
그리고 링크가 http(s):// 로 시작하지 않으면 이후 해당 링크로 이동이 안되기 때문에 http://를 추가해줍니다.
(s가 없으면 보안이 떨어지는게 아니냐고 할 수 있는데 그건 해당 웹사이트에서 자동으로 맞게 변형합니다.)
그리고 비밀번호가 틀리면 틀렸다는 말이 뜨는 HTML을 렌더링 합니다.
(예쁘게 만들 수는 있는데 사실 사이트 주인만 볼 화면이라 용량도 아낄 겸 HTML만 보내게 했습니다.)
app.post('/shorten', async(req, res) => {
if (req.body.PW == process.env.PW){
let id = null
while (1) {
id = nanoid(7)
if (await Link.exists({ identifier: id })) continue;
else break;
}
let url = req.body.URL
if (!(url.startsWith('https://') || url.startsWith('http://'))) {
url = 'http://' + url
}
await Link.create({
identifier: id,
url: url,
clicks: 0
})
res.redirect('/')
}else{
res.send(`<div style="display: flex; justify-content: center;">
<h1>Wrong Password</h1>
</div> `)
}
});
HTML 파일은 간단하게 줄일 링크를 넣을 form이랑 현재 디비에 있는 것들을 간단히 관리할 수 있는 테이블을 넣었습니다.
또한 초록 버튼을 클릭하면 링크를 복사해서 클립보드에 넣습니다.
function copy(id){
const el = document.createElement('textarea')
el.value = <link> + id
el.setAttribute('readonly', '')
el.style.position = 'absolute'
el.style.left = '-9999px'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
alert('Link Copied!')
}
빨간 버튼은 해당 document를 지우는 버튼으로 아래와 같이 구현되었습니다.
이것 또한 비밀번호를 입력해야 하며 구성은 위의 shorten과 같습니다.
app.post('/delete', (req, res) => {
if (req.body.PW == process.env.PW) {
Link.findOneAndRemove({ identifier: req.body.ID }, (err, deleted) => {
if (err) {
console.log(err)
} else {
console.log(deleted)
}
})
res.redirect('/')
}else{
res.send(`<div style="display: flex; justify-content: center;">
<h1>Wrong Password</h1>
</div> `)
}
})
마지막으로 이 프로젝트의 핵심인 다른 링크로 이동하는 코드입니다.
저기서 ':url'은 nanoid로 생성된 단축 링크로
이 단축 링크를 통해 데이터베이스에서 기존 링크를 얻어 그 링크로 이동합니다.
+비밀번호 기능을 넣다가 제가 알던 문법이랑 API 문법이 바뀐 거 같아 수정했습니다.
이제 링크가 없다고 HTML을 렌더링 합니다.
app.get('/:url', (req, res) => {
Link.findOne({ identifier: req.params.url }).then( (link) => {
if (link === null) {
res.send(`<div style="display: flex; justify-content: center;">
<h1>Link Not Found</h1>
</div> `)
}else{
link.clicks++;
link.save()
res.redirect(link.url)
}
})
})
***수정 사항***
freenom에서 무료 도메인을 받아서 연결을 해보니 문제점 몇 가지를 발견했다.
1. SSL(보안) 인증이 안 된다.(개인 목적으로 비밀번호만 다른 곳에서 쓰지 않은 것을 쓰면 큰 문제는 없다)
2. 도메인 앞에 www를 꼭 붙여야 한다.(freenom에서는 www 없이 등록이 안된다.)
결론:
CSS로 포지셔닝하는 게 오래 걸렸지 코드 자체는 간단히 구현할 수 있는 백엔드 프로젝트였다.
또한, 이번에 freenom을 사용하면서 이 프로젝트에 돈을 써가며까지 할 건 없었기에 사용했지만 집중적으로 관리할 사이트는 꼭 도메인을 구매를 해야겠다.
그래도 대부분의 기능을 하루 만에 구현한 것 치고는 깔끔하게 구현한 것 같다.