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.jsconst 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>
    );
  });
}, []);