기존에는 svg
에 사용할 모든 아이콘을 넣고 background-position
을 이용해서 아이콘을 보여주는 형식으로 사용했습니다.
하지만 사용하면서 불편함을 많이 느낀 게 background-position
도 일일이 찾아서 잡아줘야 하고, 사이즈 조절도 불편했으며, 색상 변경도 할 수 없었습니다.
이런 불편함을 겪고 남들은 어떻게 사용하나 조금 찾아보니 @svgr/wepack
로더를 사용하면 svg
파일을 컴포넌트처럼 사용할 수 있는 것을 알게 되었습니다.
( 확인해보니 index.html
에 <style>
로 들어가는 걸로 확인했습니다. )
애초에 하나의 파일에 여러 개의 이미지를 넣고 사용했던 이유가 이미지 파일을 여러 개 불러오면 요청이 성능상 안 좋다고 알고 있어서 그런 건데 컴포넌트로 분리해서 사용한다면 이미지처럼 불러와서 사용하는 것이 아니라서 나눠서 만들어서 성능상으로 문제가 없다고 생각해서 만들었던 모든 아이콘을 각각의 svg
파일로 나눠서 다시 제작했습니다.
그리고 props
로 여러 값들을 받아서 형태, 크기, hover, animation, clickEvent 등을 등록해서 독립적인 역할을 가지도록 만들었습니다.
Icon
의 props
1. shape
: 정해진 이름을 전달받으면 해당 아이콘을 그려줌 ( 기본 값: avatar
)
2. width
, height
: 아이콘의 크기 결정 ( 기본 값: 24px
)
3. fill
: 어떤 색으로 채울지 결정 ( 기본 값: black
)
4. onClick
: 이벤트 등록
5. hoverfill
: hover
시 채울 색 결정
6. animation
: 정해진 animation
이름을 주면 해당 애니메이션 실행
Icon.propTypes = {
shape: Proptypes.string,
width: Proptypes.number,
height: Proptypes.number,
fill: Proptypes.string,
onClick: Proptypes.func,
hoverfill: Proptypes.string,
animation: Proptypes.string,
};
어느정도 기본은 만들었다고 생각해서 테스트로 빌드해보고 확인해보니 HtmlWebpackPlugin
에서 만들어주는 index.html
파일에 <div id="root"></div>
가 없어서 오류가 나는걸 확인했습니다.
문제를 해결하기 위해서 구글링해서 얻은 결과가 template
이라는 속성에 기반이 될 html
파일을 넣어주면 그 html
파일을 기반으로 새로운 html
파일을 생성하는 것을 알게 되어서 추가로 index.html을 만들고 해당 파일을 넣어줬습니다.
그리고 이왕 넣어주는 김에 <meta>
도 이것저것 추가해서 간단하게 만들었습니다.
이후에 배포하기전에 자세히 공부하고 추가할 예정입니다.
// svg 파일을 위한 로더 설정 추가
{
test: /\.svg$/,
use: ["@svgr/webpack"],
}
// template 추가
new HtmlWebpackPlugin({ title: "bluegram", template: "./index.html" })
위에서 말한 아이콘을 모두 별도의 파일로 분리해서 assets
라는 폴더를 만들고 내부 icon
폴더에 모두 넣고 사용중입니다.
/src/assets
특정 게시글 모달창에서 아이콘을 클릭하면 다른 아이콘으로 바뀌도록( 좋아요 아이콘을 누르면 내부가 채워진 좋아요 아이콘으로 변경 )되도록 만들었습니다.
하지만 이상하게도 아이콘을 클릭하니 모달창이 닫히는 문제가 발생해서 원인을 찾아보니 아이콘이 변경되고 이벤트가 호출되는 순서상의 문제가 있다는 걸 알았습니다.
현재 모달창이 닫히는 원리는 window
에 click
이벤트를 달고 모달창 내부를 클릭 시 이벤트 버블링을 통해서 window
에서 이벤트를 받아서 모달창에 속해있는 노드라면 모달창이 닫히지 않고 그 반대라면 모달창이 닫히도록 구현했습니다.
하지만 모달창에서 아이콘을 클릭하면 즉시 아이콘이 다른 아이콘으로 변경되고 그 이후에 window
에서 이벤트를 받아서 내부 노드들에 속해있는지 검사하면 클릭했던 아이콘은 현재 모달창에 속해있지 않으므로 모달창이 닫히게 되는 문제가 생긴것 입니다.
약간 꼼수를 이용해서 문제를 해결했습니다.
icon
은 항상 <path>
나 <svg>
내부에 존재해서 노드의 이름을 이용해서 닫히지 않도록 구현했습니다.
// 2021/12/21 - 다른 영역 클릭 시 모달 닫기 이벤트 - by 1-blue
const handleCloseModal = useCallback(
e => {
/**
* 바로 아랫부분 추가한 이유는 게시글 모달창 내부에서 좋아요 같은 아이콘을 누르게 되면 즉시 다른 아이콘으로 변경해 줌
* ex) heart 아이콘 ==> fillHeart 아이콘
* 이때 문제가 발생하는 게 누르는 즉시 아이콘을 변경시키므로 이벤트 버블링이 돼서 widdow에서 click 이벤트를 받기 전에
* 아이콘이 변경되어버림... 그래서 모달창 내부에 있는 태그임에도 불구하고 누르면 영역 외의 태그라고 판단해서 모달창이 닫히기 때문에
* 아이콘은 특별처리로 눌러도 모달창이 닫히지 않도록 해주는 코드임
*/
if (e.target.nodeName === "path" || e.target.nodeName === "svg") return;
if (showModal && !modalRef.current?.contains(e.target)) {
setShowModal(false);
dispatch(resetPostAction());
}
},
[showModal],
);
belongsToMany
관계 즉, N : M
관계에서 include
를 사용할 경우에는 관계 테이블에 특정 작업을 하고 싶을 때는 through
를 사용하면 됩니다.
아래 코드는 User
와 Post
가 belongsToMany
관계를 맺고 있고, 중간 테이블은 likes
, User
별칭은 Liked
, Post
별칭은 Likers
로 지정한 상태입니다.
through
를 사용하지 않으면 중간 테이블의 모든 컬럼을 가지고 오게 되는데 중간 테이블을 조정하려면 아래처럼 through
를 사용하면 됩니다.
const posts = await Post.findAll({
{
model: User,
as: "Likers",
attributes: ["_id"],
through: {
attributes: ["updatedAt"],
},
}
});
많은 메서드들이 생기는 걸로 알고 있지만 실제로 사용한 메서드들만 정리하겠습니다.
아래 코드는 User
와 Post
가 belongsToMany
관계를 맺고 있고, 중간 테이블은 likes
, User
별칭은 Liked
, Post
별칭은 Likers
로 지정한 상태입니다.
const targetPost = await Post.findOne({ where: { _id: PostId } });
// 보유여부
await targetPost.hasLikers(UserId); // true or false
// 생성
const [result] = await targetPost.addLikers(UserId); // 중간 테이블(likes)에 생성한 데이터 배열로 반환
// 삭제
await targetPost.removeLikers(UserId); // 삭제된 유저의 아이디 반환
좋아요 기능을 구현하고 나서 보니 자꾸 좋아요가 반대로 생성되고 삭제되는 현상이 발생해서 원인을 찾다보니 기존에 사용했던 squelize
의 like
관련 모델 생성 코드에서 문제가 발견되었습니다.
좋아요는 N : M
관계라서 중간 테이블을 likes
로 생성했고, Workbanch
에서 확인해 보니 UserId
인 foreignKey
가 posts._id
를 가리키고 PostId
인 foreignKey
가 users._id
를 가리키는 서로 반대로 참조하는 상황이 발생했습니다.
그래서 코드를 아래로 수정했습니다.
// 기존 코드
db.Post.belongsToMany(db.User, { through: "likes", as: "Likers", foreignKey: "UserId", onDelete: "cascade" });
db.User.belongsToMany(db.Post, { through: "likes", as: "Liked", foreignKey: "PostId", onDelete: "cascade" });
// 수정 코드
db.Post.belongsToMany(db.User, { through: "likes", as: "Likers", foreignKey: "PostId", onDelete: "cascade" });
db.User.belongsToMany(db.Post, { through: "likes", as: "Liked", foreignKey: "UserId", onDelete: "cascade" });
POST
/like/post/:PostId
: 게시글에 좋아요 추가 요청DELETE
/like/post/:PostId
: 게시글에 좋아요 추가 요청404
: 존재하지 않는 게시글에 요청을 보낼 경우 응답 코드409
: 이미 눌렀거나 누르지 않은 상태에 다시 요청을 보낼 경우 응답 코드// 2021/12/25 - 좋아요 추가 - by 1-blue
router.post("/post/:PostId", isLoggedIn, async (req, res, next) => {
const PostId = +req.params.PostId;
const { _id: UserId } = req.user;
try {
const targetPost = await Post.findOne({ where: { _id: PostId } });
// 2021/12/25 - 존재 하지 않는 게시글에 좋아요 누른 경우 - by 1-blue
if (!targetPost) {
return res
.status(404)
.json({ message: "존재하지 않는 게시글에 좋아요를 누르셨습니다.\n새로 고침 후 다시 시도해 주세요" });
}
// 2021/12/25 - 좋아요를 누른 게시글에 다시 좋아요 추가 요청인 경우 - by 1-blue
if (await targetPost.hasLikers(UserId)) {
return res.status(409).json({ message: "이미 좋아요를 누른 게시글입니다.\n새로 고침 후 다시 시도해 주세요." });
}
// 2021/12/25 - 정상적으로 좋아요 추가 - by 1-blue
const [result] = await targetPost.addLikers(UserId);
res.json({ message: `${req.user.name}님 게시글에 좋아요를 누르셨습니다.`, result });
} catch (error) {
console.error("POST /like/:PostId >> ", error);
next(error);
}
});
sequelize
를 어느 정도 공부하고 프로젝트에 적용한다고 생각했는데 기본 세팅부터 잘못한 걸 보고 아직 부족하다는 것을 느꼈습니다.
내일은 개발보다는 sequelize
에 대한 이론 공부에 집중해 볼 생각입니다.