β Business Model - Diver provides collections and reviews of books and users can share them like in a social network.
π Base - A Node.js + Express based backend project.
π Our frontend - Click Frontend side to go to corresponding React.js based frontend project.
At the beginning of the project, deciding which part to focus and which part to discard – in terms of the tech stack – was the most difficult task.
During previous projects and tutorials, we could glimpse several basic techs including Application, Database, DevOps and/or Business tools.
Yet, with not enough time to master everything that we learned, here are the things that we wanted to put an emphasis on.
νλ‘μ νΈλ₯Ό μμν λ μ΄λ€ κΈ°μ μ€νμ μ§μ€νκ³ μ΄λ€ κ²μ μ μ³λμ§ μ νλ κ²μ΄ κ°μ₯ μ΄λ €μ μ΅λλ€.
μ΄μ μ νλ‘μ νΈλ€κ³Ό νν 리μΌμμ λͺκ°μ§ μ΄ν리μΌμ΄μ , λ°μ΄ν°λ² μ΄μ€, λ°λΈμ΅μ€ κ·Έλ¦¬κ³ λΉμ¦λμ€ ν΄λ€μ μ ν μ μμμ΅λλ€λ§,
λ°°μ΄ κ²λ€μ λͺ¨λ μλ¬νκΈ°μλ 짧μ μκ°μ΄μκΈ° λλ¬Έμ μλμ λͺ©λ‘μ μ£Όμμ μ λκΈ°λ‘ νμμ΅λλ€.
.
βββ ...
βββ controllers
| βββ book.controller.js
| βββ collection.controller.js
| βββ crawl.controller.js
| βββ get_collection_image.js
| βββ get_trending_review.js
| βββ image_upload.controller.js
| βββ review.controller.js
| βββ schedule.controller.js
| βββ super.controller.js
| βββ tag.controller.js
| βββ utilities.js
|
βββ middleware
| βββ auth_middleware.js
|
βββ models
| βββ alert.js
| βββ book.js
| βββ changes_index.js
| βββ collection.js
| βββ comment.js
| βββ follow.js
| βββ index.js
| βββ review.js
| βββ suggestion.js
| βββ trend.js
| βββ user.js
| βββ utilities.js
|
βββ models
| βββ alert.js
| βββ book.js
| βββ changes_index.js
| βββ collection.js
| βββ comment.js
| βββ follow.js
| βββ index.js
| βββ review.js
| βββ suggestion.js
| βββ trend.js
| βββ user.js
| βββ utilities.js
|
βββ routes
| βββ book.js
| βββ collection.js
| βββ comments.js
| βββ feeds.js
| βββ follow.js
| βββ google_passport.js
| βββ index.js
| βββ kakao_passport.js
| βββ review.js
| βββ search.js
| βββ suggestion.js
| βββ user.js
|
βββ app.js
βββ config.js
βββ exp_list.js
βββ server.js
βββ socket.js
βββ ...
Stage | Within 7 days | Followed User's review | Likes |
---|---|---|---|
1. Recent unread reviews of following users. | π’ | π’ | β |
2. Trending reviews (reviews with high trending point, in other words, recent review with lots of likes) | π’ | β | π’ |
3. All recent unread reviews regardless of following. | π’ | β | β |
Extra: Show all reviews (Provided on a different tab) | β | β | π’ |
Not only using basics of ES6 superficially, we tried to implement syntactic sugars of recent versions of ECMAScript.
Here are some codes that went through refactoring to apply ES6+
ES6+ μ κΈ°λ³ΈλΏλ§ μλλΌ μ΅μ λ²μ ECMAScript μ λ¬Έλ²μ μ€νμ μ μ©νκΈ° μν΄μ λ Έλ ₯νμ΅λλ€.
μλλ ES6+ μ μ©μ νμ¬ λ¦¬ν©ν λ§μ μ§νν μ½λμ λλ€.
To reduce time consumption on crawling, we implemented Promise.allSettled().
Because we can query on anytime later, being rejected on some requests was not a big deal – and this DOES happen due to advertisement in source URL.
ν¬λ‘€λ§ μ μκ° μμλ₯Ό μ€μ΄κΈ° μν΄ Promise.allSettled() λ₯Ό μ¬μ©νμμ΅λλ€.
λμ€μλΌλ λ€μ λ°μμ€λ©΄ λκΈ° λλ¬Έμ λͺκ° μ€ν¨νλ€κ³ νλλΌλ ν° λ¬Έμ κ° μλμκΈ° λλ¬Έμ λλ€. μ€ν¨λ λμ νμ΄μ§μ μ½μ λ μκΈ°μΉ λͺ»ν κ΄κ³ λλ¬Έμ΄μμ΅λλ€.
// ./controllers/crawl.js
// todo Deprecated example
const getBestsellers = async () => {
const BESTSELLER_URL = 'https://www.kyobobook.co.kr/bestSellerNew/bestseller.laf'
const query = 'ul > input[name=barcode]'
const page = await launchBrowserAndGotoURL(BESTSELLER_URL)
const isbnArr = await page.$$eval(query, (inputList) => inputList.map((input) => input.value))
await (await page.browser()).close()
const top10 = isbnList.slice(0, 9)
const promises = top10.map((isbn) => searchBooks('isbn', isbn))
return [...await Promise.allSettled(promises)].filter((p) => p.status === 'fulfilled').map((p) => p.value)
}
Optional Chaining was powerful and simple operator to process optional parameters. It created more readable code
than if
statement.
μ΅μ
λ 체μ΄λμ μ νμ μΈ λ§€κ°λ³μλ₯Ό μ²λ¦¬νκΈ°μ κ°λ ₯νκ³ κ°κ²°ν μ°μ°μμ
λλ€.if
λ¬Έμ λΉν΄ λ μ½κΈ° μ’μ μ½λκ° λμμ΅λλ€.
// Saving a review with / without an image.
// Previously
let image
if (res.locals) image = res.locals.url
await Review.create({ content, quote, image })
// Refactored
const image = res.locals?.url
await Review.create({ content, quote, image })
??
operator also helped to simply a complex if
statement and reduce use of let
.
??
μ°μ°μ μμ 볡μ‘ν if
λ¬Έμ κ°λ¨νκ² ν΄μ£Όκ³ let
μ λ μ¬μ©νκ² ν΄μ£Όμμ΅λλ€.
// Find a collection document by its name.
// If such document doesn't exist, create one.
// Previously
const updateCollection = async (tag) => {
let collection
collection = await Collection.findOne({ name: tag, type: 'tag' })
if (!collection) collection = await Collection.create({ name: tag, type: 'tag ' })
//...
}
// Refactored
const updateCollection = async (tag) => {
const collection = await Collection.findOne({ name: tag, type: 'tag' }) ?? await Collection.create({
name: tag,
type: 'tag'
})
//...
}
By using async/await
, it was possible to avoid complex call backs and use try/catch
to handle errors.
async/await
μ μ¬μ©νμ¬ λ³΅μ‘ν μ½λ°± ꡬ쑰λ₯Ό νΌνκ³ try/catch
λ‘ μλ¬λ₯Ό μ²λ¦¬ν μ μμμ΅λλ€.
export default class ReviewController extends SuperController {
//...
static async apiDeleteReview(req, res, next) {
const { _id: userId } = res.locals.user
try {
const { reviewId } = ReviewController._getIds(req)
const review = await Review.findById(reviewId)
if (!review) return next({ message: 'μ‘΄μ¬νμ§ μλ 리뷰 μμ΄λ μ
λλ€.', status: 400 })
ReviewController._validateAuthor(review.user, userId)
await review.deleteOne()
return res.sendStatus(202)
} catch (err) {
console.error(err)
if (err.status) next(err)
return next({ message: '리뷰 μμ λ₯Ό μ€ν¨νμ΅λλ€.', status: 500 })
}
}
}
By adopting ESModule, modules are loaded both asynchronously and partially. Thus saving memory and time.
ES λͺ¨λμ μ¬μ©νμ¬ λͺ¨λμ λΉ-λκΈ°μ κ·Έλ¦¬κ³ λΆλΆμ μΌλ‘ λΆλ¬μ¨λ€. λ°λΌμ λ©λͺ¨λ¦¬λ₯Ό μλΌκ³ μλλ₯Ό ν₯μμν¬ μ μλ€.
Object-oriented programming is possible with class
in some degree. In Diver backend, both reviews and collections
share common features: they need a user to be logged in to post, they are MongoDB documents, they are both related to
book and so on.
Therefore, Super class inherits shared common static methods – which I wish to be protected methods but current JS doesn't support such feature – to ReviewController and CollectionController classes.
And controllers related to the same MongoDB collections – Review and Collection – are grouped into each class.
export default class SuperController {
static _getIds(req) {
//...
}
static _validateAuthor(author, currentUserId) {
//...
}
}
// review.controller.js
import SuperController from './super.controller.js'
export default class ReviewController extends SuperController {
static async apiPostReview(req, res, next) {
//...
}
//...
}
// routes/reviews.js
//...
import ReviewCtrl from './review.controller.js'
router.route('/')
.post(upload.single('image'), ImageUpload.uploadImage, ReviewCtrl.apiPostReview)
.get(ReviewCtrl.apiGetReviews)
router.route('/:reviewId')
.get(ReviewCtrl.apiGetReview)
.put(ReviewCtrl.apiPutReview)
.delete(ReviewCtrl.apiDeleteReview)
//...
Formerly, complex document manipulation was done in Node.js server.
router.get('/feeds', async (req, res, next) => {
const { _id: userId } = res.locals.user
try {
let user = await User.findById(userId)
// User .followCount method to create .followingCount and .followerCount properties.
user = await user.followCount()
const reviews = await Review.find({ user: userId }).populate('book').sort('-created_at')
const collections = await Collection.find({ user: userId }).sort('-created_at')
return res.json({ user, reviews, collections })
} catch (e) {
console.error(e)
return next(new Error('κ°μΈ νΌλ λΆλ¬μ€κΈ°λ₯Ό μ€ν¨νμ΅λλ€.'))
}
})
Later, the code was refactored by using aggregation pipelines. So the process is now done in MongoDB server (Mongo Atlas).
router.get('/feeds', async (req, res, next) => {
const { _id: userId } = res.locals.user
const query = {
//...
}
const projection = {
//...
}
// Using $lookup to calculate follower / following counts.
const followAggregation = [
{
'$lookup': {
'from': 'follows',
'let': { 'id': '$_id' },
'pipeline': [
{
'$match': {
'$expr': { '$eq': ['$$id', '$sender'] },
},
},
{ '$count': 'count' },
],
'as': 'followerCount',
},
}, {
'$lookup': {
'from': 'follows',
'let': { 'id': '$_id' },
'pipeline': [
{
'$match': {
'$expr': { '$eq': ['$$id', '$receiver'] },
},
},
{ '$count': 'count' },
],
'as': 'followingCount',
},
}, {
'$addFields': {
'followerCount': {
'$sum': '$followerCount.count',
},
'followingCount': {
'$sum': '$followingCount.count',
},
},
},
]
try {
// Simpler code with aggregation.
const user = await User.aggregate([query, projection, ...followAggregation])
const reviews = await Review.find({ user: userId }).populate('book').sort('-created_at')
const collections = await Collection.find({ user: userId }).sort('-created_at')
return res.json({ user, reviews, collections })
} catch (e) {
console.error(e)
return next(new Error('κ°μΈ νΌλ λΆλ¬μ€κΈ°λ₯Ό μ€ν¨νμ΅λλ€.'))
}
})
κΉμΉλΉ |
κΆμ€λΉ |