Next.js
์Node.js
์Sequelize
๋ฅผ ์ด์ฉํด ํด์ํ๊ทธ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ํฌ์คํธ์ ๋๋ค.
๋ค์ด๊ฐ๊ธฐ ์ ์ ํด์ํ๊ทธ์ ๊ด๊ณ๋ฅผ ๋งบ์ ๋์์ ๋ํด์ ๋จผ์ ์๊ฐํด๋ณด๊ฒ ์ต๋๋ค.
์ฝ๊ฒ ์๊ฐํด๋ณด๋ฉด ํด์ํ๊ทธ๋ฅผ ์์ฑํ ๋ ํด๋น ๊ฒ์๊ธ์ ํด์ํ๊ทธ ์ปฌ๋ผ์ ๋ง๋ค๊ณ ๋ชจ๋ ๋ฃ์ด๋ฒ๋ฆฌ๋ฉด ๋ฉ๋๋ค.
์ด๋ ๊ฒ ๋ง๋ค๋ฉด ์๊ท๋ชจ๋ก ์ด์๋๋ค๋ฉด ํฌ๊ฒ ๋ฌธ์ ๋ ์๋ค๊ณ ์๊ฐํฉ๋๋ค. ํ์ง๋ง ๊ท๋ชจ๊ฐ ์ปค์ง์๋ก DB
์ ์ค๋ณต๋ ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
์๋ฅผ ๋ค์ด #์ค๋
์ด๋ผ๋ ํด์ํ๊ทธ๋ฅผ A, B, C๋ผ๋ ๊ฒ์๊ธ์ ๋ชจ๋ ์ฌ์ฉํ๋ค๊ณ ๊ฐ์ ํ๋ฉด A, B, C ๋ชจ๋ ํ
์ด๋ธ์ ํด์ํ๊ทธ ์ปฌ๋ผ์ #์ค๋
์ด๋ผ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฃ๋ ์ค๋ณต์ด ๋ฐ์ํฉ๋๋ค.
์ด๋ฐ ์ค๋ณต์ ์ ๊ฑฐํ๊ธฐ ์ํด์ ํด์ํ๊ทธ์ ๊ฒ์๊ธ์ N:M
์ผ๋ก ๊ตฌํํ์ต๋๋ค.
N:M
๊ด๊ณ๋ ํ์ฐ์ ์ผ๋ก ์ค๊ฐ ํ
์ด๋ธ์ด ์๊ธฐ๊ฒ ๋ฉ๋๋ค.
1:1
์ธ ๊ฒฝ์ฐ๋ ์ํ๋ ๋ชจ๋ธ์ foreign key
๋ฅผ ๋ฃ์ผ๋ฉด ๋๊ณ , 1:N
์ธ ๊ฒฝ์ฐ๋ N
์ชฝ(ํฌํจ๋๋ ์ชฝ)์ foreign key
๋ฅผ ๋ฃ์ผ๋ฉด ๋ฉ๋๋ค.
N:M
์ผ ๊ฒฝ์ฐ์ ์ค๊ฐ ํ
์ด๋ธ ์์ด ์์ชฝ์ ๋ค ๋ฃ๋๋ค๊ณ ๊ฐ์ ํด๋ณด๋ฉด ํ ๋ฒ์ ๊ด๊ณ์ผ ๋๋ ๊ด์ฐฎ์ง๋ง ์ฌ๋ฌ ๊ฐ๊ฐ ์ฐ๊ฒฐ๋ ๊ฒฝ์ฐ์๋ ํ๋์ ์ปฌ๋ผ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๊ฒ ๋ฉ๋๋ค.
๋ฐ๋ผ์ ์ค๊ฐ ํ
์ด๋ธ์ ์ด์ฉํด์ ์ฒ๋ฆฌํฉ๋๋ค. ์ค๊ฐ ํ
์ด๋ธ์๋ ์์ชฝ์ foreign key
๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์ด๊ฐ๋๋ค. ๊ทธ ์์ชฝ์ foreign key
์ ๋ช
์นญ์ ์๋์ ๋ชจ๋ธ ์ ์์ foreignKey
์์ ์ ํฉ๋๋ค. ๋ํ through
๋ ์ค์ ์์ฑ๋ ์ค๊ฐ ํ
์ด๋ธ์ ๋ช
์นญ์ ์ ํ๋ ๊ฒ์ด๊ณ , as
๋ ์ฝ๋์์์ ์ฌ์ฉํ ์ค๊ฐ ํ
์ด๋ธ์ ์ด๋ฆ์ ์ ํ๋ ๊ฒ(+ ์์ฑ๋ ํฌํผ ๋ฉ์๋์ ์ด๋ฆ ์ง์ )์
๋๋ค.
Sequelize
์์ ๊ด๊ณ๋ฅผ ์ง์ ํ๋ฉด ๊ด๊ณ์ ๋ง๊ฒ ์๋์ผ๋ก ๋ช ๊ฐ์ง์ ํฌํผ ๋ฉ์๋๋ฅผ ์ ๊ณตํด์ค๋๋ค.models/post.js
const Post = (sequelize, DataTypes) => {
const Post = sequelize.define(
"Post",
{
_id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "๊ฒ์๊ธ์ ์์ด๋ ( ๊ฒ์๊ธ์ ์๋ณํ ๊ฐ )",
},
content: {
type: DataTypes.STRING(2200),
alllowNull: true,
comment: "๊ฒ์๊ธ์ ๋ด์ฉ ( ์ต๋ 2200์, , ํน์๋ฌธ์ ๊ฐ๋ฅ )",
},
},
{
sequelize,
timestamps: true,
paranoid: false,
underscored: false,
modelName: "Post",
tableName: "posts",
charset: "utf8mb4",
collate: "utf8mb4_general_ci",
},
);
Post.associate = db => {
// ํด์ํ๊ทธ ( N : M ) ( ๊ฒ์๊ธ๊ณผ ํด์ํ๊ทธ )
db.Post.belongsToMany(db.Hashtag, {
through: "PostHashtags",
as: "postHashtager",
foreignKey: "PostId",
onDelete: "cascade",
});
// ... ๋๋จธ์ง ๋ค๋ฅธ ๊ด๊ณ๋ค์ ์๋ต
};
return Post;
};
export default Post;
const Hashtag = (sequelize, DataTypes) => {
const Hashtag = sequelize.define(
"Hashtag",
{
_id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "ํด์ํ๊ทธ์ ์์ด๋ ( ํด์ํ๊ทธ์ ์๋ณํ ๊ฐ )",
},
content: {
type: DataTypes.STRING(40),
alllowNull: true,
comment: "ํด์ํ๊ทธ์ ๋ด์ฉ ( ์ต๋ 40์, ํน์๋ฌธ์ ๋ถ๊ฐ๋ฅ )",
},
},
{
sequelize,
timestamps: true,
paranoid: false,
underscored: false,
modelName: "Hashtag",
tableName: "hashtags",
charset: "utf8",
collate: "utf8_general_ci",
},
);
Hashtag.associate = db => {
// ํด์ํ๊ทธ ( N : M ) ( ๊ฒ์๊ธ๊ณผ ํด์ํ๊ทธ )
db.Hashtag.belongsToMany(db.Post, {
through: "PostHashtags",
as: "PostHashtaged",
foreignKey: "HashtagId",
onDelete: "cascade",
});
};
return Hashtag;
};
export default Hashtag;
์ฌ์ฉ์๋ ์ผ๋ฐ ํ
์คํธ์ ํด์ํ๊ทธ๋ฅผ ์์ด์ ๊ฒ์๊ธ ์์ฑ ์์ฒญ์ ๋ณด๋ด๊ธฐ ๋๋ฌธ์ ํ
์คํธ ์ค์์ ํด์ํ๊ทธ๋ฅผ ์ถ์ถํ ์ ์์ด์ผ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ํญ์ ์ ์์ ์ผ๋ก ํด์ํ๊ทธ๋ฅผ ์์ฑํ์ง ์๊ธฐ ๋๋ฌธ์ ํน์ดํ๊ฒ ์๊ธด ํด์ํ๊ทธ์กฐ์ฐจ๋ ์ ๋๋ก ์ถ์ถํ ์ ์๊ฒ ์ ๊ท ํํ์์ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
์๋ฅผ ๋ค์ด ###ํด์ํ๊ทธ
, #ํด#ํด์
์ ๊ฐ์ ๊ฒฝ์ฐ #ํด์ํ๊ทธ
, #ํด
, #ํด์
์ ๊ฐ์ด ์ถ์ถํ ์ ์์ด์ผ ํฉ๋๋ค.
์์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ ๊ท ํํ์์ /#[a-z0-9_๊ฐ-ํฃ]+/gm
์
๋๋ค. ์ฌ๊ธฐ์ String.prototype.match(์ ๊ทํํ์)
์ ์ด์ฉํด์ ์ํ๋ ํด์ํ๊ทธ๋ง ์ถ์ถํฉ๋๋ค.
/routes/post.js
// ์ด๋ฏธ์ง, ๋๊ธ, ์ ์ ์ฒ๋ผ ํ ํฌ์คํธ์ ์ฃผ์ ์ ๋ฒ์ด๋๋ ๋ด์ฉ์ ๋ชจ๋ ์๋ตํ๊ฒ ์ต๋๋ค.
import db from "../models/index.js";
const { Post, Hashtag } = db;
// 2021/12/22 - ๊ฒ์๊ธ ์์ฑํ๊ธฐ - by 1-blue
router.post("/", async (req, res, next) => {
const { content } = req.body;
try {
// ๊ฒ์๊ธ ์์ฑ
const createdPost = await Post.create({ content, UserId: req.user._id });
// ํด์ํ๊ทธ ์ถ์ถ
const hashtags = content.match(/#[a-z0-9_๊ฐ-ํฃ]+/gm);
if (hashtags) {
// ํด์ํ๊ทธ๋ค ์์ฑ
const hashtagPromiseList = hashtags.map(hashtag => {
// ๋งจ ์ "#"์ ๊ฑฐ
const content = hashtag.substr(1, hashtag.length);
// ์ด๋ฏธ ์กด์ฌํ๋ฉด ๊ฐ์ ธ์ค๊ณ ์์ผ๋ฉด ์์ฑ
return Hashtag.findOrCreate({ where: { content } });
});
const results = await Promise.all(hashtagPromiseList);
// ๊ฒ์๊ธ๊ณผ ํด์ํ๊ทธ ์ค๊ฐ ํ
์ด๋ธ์ ์ปฌ๋ผ ์์ฑ ( ๊ฒ์๊ธ๊ณผ ํด์ํ๊ทธ ์ฐ๊ฒฐ )
const hashtagPostPromiseList = results.map(hashtag => hashtag[0].addPostHashtaged(createdPost._id));
await Promise.all(hashtagPostPromiseList);
}
return res
.status(201)
.json({ ok: true, message: "๊ฒ์๊ธ์ ์ฑ๊ณต์ ์ผ๋ก ์์ฑํ์ต๋๋ค." });
} catch (error) {
console.error("POST /post error >> ", error);
return next(error);
}
});
/routes/posts.js
// ํ์ด์ง ๋ค์ด์
, ์ ์ , ๋๊ธ ์ฒ๋ผ ํ ์ฃผ์ ์ ๋ฒ์ด๋๋ ๋ด์ฉ์ ์ ์ธํ์ต๋๋ค.
router.get("/hashtag/:hashtagText", async (req, res, next) => {
const hashtagText = decodeURI(req.params.hashtagText);
try {
// ํน์ ํด์ํ๊ทธ ์ฐพ๊ธฐ
const hashtag = await Hashtag.findOne({
where: { content: hashtagText },
});
if (!hashtag)
return res.status(200).json({
ok: true,
message: "ํด์ํ๊ทธ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.",
limit,
posts: [],
postCount: 0,
hashtag: hashtagText,
});
// ์๋ ๋ ๊ฐ์ ๋ฉ์๋ ์ด๋ฆ์ด "PostHashtaged"์ธ ์ด์ ๋ ์ด์ ์ "as"๋ฅผ ์ด์ฉํด์ ์ ์ํด์ค ์ด๋ฆ์ด๊ธฐ ๋๋ฌธ
// ํน์ ํด์ํ๊ทธ์ ๊ด๊ณ์๋ ๊ฒ์๊ธ๋ค ๊ฐ์ ธ์ค๊ธฐ
const postsOfHashtag = await hashtag.getPostHashtaged({
attributes: ["_id", "createdAt", "content", "UserId"],
});
// ํน์ ํด์ํ๊ทธ์ ๊ด๊ณ์๋ ๋ชจ๋ ๊ฒ์๊ธ ๊ฐ์ ๊ตฌํ๊ธฐ ( ์๋๋ 10๊ฐ์ฉ ๋ถํ ์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์ ์ด ๊ฐ์๋ฅผ ๋ฐ๋ก ๊ตฌํ๋ ๊ฒ )
const postsOfHashtagCount = await hashtag.countPostHashtaged();
res.status(200).json({
ok: true,
posts: postsOfHashtag,
postCount: postsOfHashtagCount,
hashtag: hashtagText,
});
} catch (error) {
console.error("GET /post/hashtag/:hashtag error >> ", error);
return next(error);
}
});
// ๋ ๋๋งํ ๊ฒ์๊ธ์ ๋ด์ฉ์ ํด์ํ๊ทธ๊ฐ ์กด์ฌํ๋ค๋ฉด ํด์ํ๊ทธ์ ๋งํฌ๋ฅผ ๋ฌ์์ ํด๋ฆญ ์ ํด๋น ํด์ํ๊ทธ๋ฅผ ํฌํจํ๋ ๊ฒ์๊ธ์ ์ฐพ๋ ํ์ด์ง๋ก ์ด๋ํ๊ฒ ๋ง๋ฆ
const preprocessHashtag = useCallback((contents: string) => {
return contents.split(/(#[^\s#]+)/gm).map((text) => {
if (text[0] !== "#") return text;
return (
<Link
key={text}
href={`/hashtag/${encodeURI(text.substr(1, text.length))}`}
>
<a className="post-card-content-hashtag">{text}</a>
</Link>
);
});
}, []);