[project] ๐ŸŽฌMovyes: ์˜ํ™” ์ปค๋ฎค๋‹ˆํ‹ฐ React ํ”„๋กœ์ ํŠธ - ์ปค๋ฎค๋‹ˆํ‹ฐ

์˜ค์˜ค๊ตฌยท2023๋…„ 1์›” 10์ผ
0

์ปค๋ฎค๋‹ˆํ‹ฐ

๊ด€์‹ฌ์žˆ๋Š” ์˜ํ™”๋ฅผ ๋ชจ์•„๋‘˜ ์ˆ˜ ์žˆ๋‹ค.
ํŽธ์˜์ƒ ์—ฌ๊ธฐ์„  '์ฐœ' ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ๋ถ€๋ฅด๊ฒ ๋‹ค.


์„ธ๋ถ€ ๊ธฐ๋Šฅ

  • ๊ธ€ ์ž‘์„ฑ
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ฐ€๋Šฅ
    • ์œ ํšจ์„ฑ๊ฒ€์‚ฌ: ๋‚ด์šฉ์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์—…๋กœ๋“œ ๋ถˆ๊ฐ€๋Šฅ
  • ๊ธ€ ์กฐํšŒ
    • ์ด๋ฏธ์ง€ ์ž‘๊ฒŒ ๋‚˜์˜ค๊ณ  ํด๋ฆญํ•˜๋ฉด ์›๋ณธ ์‚ฌ์ด์ฆˆ ๋‚˜์˜ด
  • ๊ธ€ ์‚ญ์ œ
    • ๋ณธ์ธ์ด ์ž‘์„ฑํ•œ ๊ธ€๋งŒ [์‚ญ์ œ] ๋ฒ„ํŠผ ๋…ธ์ถœ
  • ๊ธ€ ์ˆ˜์ •
    • ๋ณธ์ธ์ด ์ž‘์„ฑํ•œ๊ธ€๋งŒ [์ˆ˜์ •] ๋ฒ„ํŠผ ๋…ธ์ถœ
    • ์ˆ˜์ •์‹œ์—” ์ƒˆ๋กœ์šด ์‚ฌ์ง„ ์ถ”๊ฐ€๋‚˜ ์‚ฌ์ง„ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅ(์‚ญ์ œ๋Š” ๊ฐ€๋Šฅ)

์•„ํ‚คํƒ์ฒ˜

ํด๋”๊ตฌ์กฐ

๐Ÿ“ฆsrc
 โ”ฃ ๐Ÿ“‚api
 โ”ƒ โ”ฃ ๐Ÿ“œfirebase.jsx // realtime databaase API
 โ”ƒ โ”— ๐Ÿ“œupload.jsx // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ์šฉ storage API
 โ”ฃ ๐Ÿ“‚components
 โ”ƒ โ”ฃ ๐Ÿ“‚community
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œDeletePost.jsx // ์‚ญ์ œ ์ปดํฌ๋„ŒํŠธ
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œNewPost.jsx // ์ƒˆ ๊ธ€ ์ž‘์„ฑ ์ปดํฌ๋„ŒํŠธ
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œPostCard.jsx // ๊ฐœ๋ณ„ ๊ฒŒ์‹œ๊ธ€   
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œPostList.jsx // ์ „์ฒด ๊ฒŒ์‹œ๊ธ€
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œPostModal.jsx // ์ˆ˜์ •, ์‚ญ์ œ ์ฐฝ์ด ์ถœ๋ ฅ๋˜๋Š” ๋ชจ๋‹ฌ
 โ”ฃ ๐Ÿ“‚pages
 โ”ƒ โ”ฃ ๐Ÿ“œCommunity.jsx // ์ปค๋ฎค๋‹ˆํ‹ฐ ํŽ˜์ด์ง€
 โ”ฃ ๐Ÿ“‚store
 โ”ƒ โ”ฃ ๐Ÿ“‚community
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“œcommunity-actions.jsx
 โ”ƒ โ”ƒ โ”— ๐Ÿ“œcommunity-slice.jsx
 โ”ƒ โ”— ๐Ÿ“œstore.jsx

๋ผ์šฐํŒ…

path: "/community" element: "<Community/>"

๊ณ ๋ฏผ

1. DB๊ตฌ์กฐ

์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€ CRUD๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ์— ์•ž์„œ DB๊ตฌ์กฐ(๊ฒฝ๋กœ)๋ฅผ ์–ด๋–ป๊ฒŒ ์งค ๊ฒƒ์ธ๊ฐ€ ์ƒ๊ฐํ•ด์•ผ๋๋‹ค. ๋‹จ์ˆœํžˆ ๊ฒŒ์‹œ๊ธ€๋งŒ ์ž‘์„ฑํ•˜๋Š”๊ฑด ๊ฒฝ๋กœ๊ฐ€ ํฌ๊ฒŒ ์ค‘์š”ํ•˜์ง€ ์•Š๊ฒ ์ง€๋งŒ ๋‚˜๋Š” ํ›„์— ๋Œ“๊ธ€ ๊ธฐ๋Šฅ๋„ ๊ตฌํ˜„ํ•  ์˜ˆ์ •์ด๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋–ป๊ฒŒํ•˜๋ฉด ๋‘ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ๊ธฐ์ ์œผ๋กœ ์ž˜ ์—ฐ๊ฒฐ๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์„๊นŒ ๊ณ ๋ฏผํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๋– ์˜ฌ๋ ธ๋‹ค.

1. ๊ฒŒ์‹œ๊ธ€์ด ๋Œ“๊ธ€ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จ

/community/posts/๊ฒŒ์‹œ๊ธ€id/๊ฒŒ์‹œ๊ธ€๋‚ด์šฉ,๋Œ“๊ธ€

์ด๋ ‡๊ฒŒํ•˜๋ฉด ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์„ ๋“ค๊ณ ์˜ค๋ฉด ์ž๋™์œผ๋กœ ๋ชจ๋“  ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋˜๋ฏ€๋กœ ๊ฒŒ์‹œ๊ธ€๊ณผ ๋Œ“๊ธ€ ์กฐํšŒ ๊ธฐ๋Šฅ์€ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•˜๋‹ค.

ํ•˜์ง€๋งŒ ๊ฒŒ์‹œ๊ธ€์„ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ ํ• ๋•Œ๋งˆ๋‹ค ๊ฒŒ์‹œ๊ธ€์˜ ๋ชจ๋“  ์ •๋ณด๊นŒ์ง€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜๊ณ  ๋Œ“๊ธ€์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด์„  ๊ฒŒ์‹œ๊ธ€ ์ž์ฒด์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๋น„ํšจ์œจ ์ ์ด๋ผ๊ณ  ๋А๊ผˆ๋‹ค.

2. ๊ฒŒ์‹œ๊ธ€๊ณผ ๋Œ“๊ธ€์„ ๊ฐ์ž ๋‹ค๋ฅธ ๊ฒฝ๋กœ์— ์ €์žฅ

๊ฒŒ์‹œ๊ธ€: /community/posts/๊ฒŒ์‹œ๊ธ€id/๊ฒŒ์‹œ๊ธ€๋‚ด์šฉ
๋Œ“๊ธ€: /community/comment/๊ฒŒ์‹œ๊ธ€id/๋Œ“๊ธ€๋‚ด์šฉ

์‹ค์ œ ๊ตฌํ˜„ํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค.

๊ฒŒ์‹œ๊ธ€๊ณผ ๋Œ“๊ธ€์„ ์„œ๋กœ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์— ์ €์žฅํ•˜๊ณ , ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•  ๋•Œ /community/comment/๊ฒŒ์‹œ๊ธ€id ๊ฒฝ๋กœ์—์„œ ๋Œ“๊ธ€์„ ๊ฐ€์ ธ์˜จ๋‹ค.

์ด๋Ÿฌ๋ฉด ๊ฒŒ์‹œ๊ธ€์„ ์—…๋ฐ์ดํŠธ ํ•  ๋• ๊ฒŒ์‹œ๊ธ€ ๊ฒฝ๋กœ๋งŒ ์ ‘๊ทผํ•˜๋ฉด ๋˜๊ณ , ๋Œ“๊ธ€์„ ์—…๋ฐ์ดํŠธ ํ•  ๋• ๋Œ“๊ธ€ ๊ฒฝ๋กœ์—๋งŒ ์ ‘๊ทผํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ ๋ถˆํ•„์š”ํ•œ ์—…๋ฐ์ดํŠธ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•˜๋‹ค.

์ถ”๊ฐ€๋กœ, ๊ฒฝ๋กœ์— userId๋ฅผ ์ถ”๊ฐ€ํ• ๊นŒ๋„ ์ƒ๊ฐํ•ด๋ดค๋Š”๋ฐ ๋Œ“๊ธ€์ด๋‚˜ ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ๊ฐ€ auth(์ž‘์„ฑ์ž) ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ฏ€๋กœ ๊ตณ์ด ๊ฒฝ๋กœ์— user์ •๋ณด๊นŒ์ง€ ์ถ”๊ฐ€ํ•  ํ•„์š”๋Š” ์—†๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.


2. ์ด๋ฏธ์ง€๋ฅผ ํฌํ•จํ•œ ๊ฒŒ์‹œ๊ธ€์„ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•

์ด๋ฏธ์ง€๊ฐ€ ์ฒจ๋ถ€๋œ ๊ฒŒ์‹œ๊ธ€์„ ๋‹ค๋ฃจ๋Š”๊ฒŒ ์€๊ทผ ๊นŒ๋‹ค๋กญ๋‹ค๊ณ  ๋А๊ปด์กŒ๋‹ค. ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค์—๋Š” ์ด๋ฏธ์ง€ ์ž์ฒด๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์˜ˆ์ „์— ์ž๋ฐ”์™€ ์Šคํ”„๋ง์œผ๋กœ ๋ฐฑ์—”๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•  ๋•Œ ์ด๋ฏธ์ง€๋ฅผ ๋‹ค๋ฃจ๋Š”๋ฐ ๊ฝค ์• ๋ฅผ ๋จน์—ˆ๋˜ ๊ธฐ์–ต์ด ์žˆ์–ด์„œ ์ด๋ฒˆ์—๋„ ๊ธด์žฅ๋ถ€ํ„ฐ ๋์ง€๋งŒ ๋ง‰์ƒ ๋ฐฉ๋ฒ•์„ ์•Œ์•„ ๋ณด๋‹ˆ ๊ทธ๋‹ฅ ์–ด๋ ต์ง€๋Š” ์•Š์•˜๋‹ค.

1. ์ €์žฅ

์ด๋ฏธ์ง€๊ฐ€ ํฌํ•จ๋œ ๊ฒŒ์‹œ๊ธ€์„ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ €
1. ์ด๋ฏธ์ง€๋ฅผ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์‹œํ‚ค๊ณ 
2. ์—…๋กœ๋“œํ•œ ์„œ๋ฒ„url์„ ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ์— ์ €์žฅํ•œ๋‹ค

์ฆ‰, ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ์—๋Š” ์ด๋ฏธ์ง€ ์ž์ฒด๊ฐ€ ์ €์žฅ๋˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ ๋œ url๊ฒฝ๋กœ๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

๋‚˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ•  ์„œ๋ฒ„๊ฐ€ ์—†์–ด ๋˜๋‹ค์‹œ firebase์˜ ๋„์›€์„ ๋ฐ›์•„์•ผ ํ–ˆ๋Š”๋ฐ realtime database์—๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— storage๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

storage๋Š” realtime database์™€ ๊ฑฐ์˜ ๋น„์Šทํ•œ๋ฐ ๊ทธ ํ™œ์šฉ์„ฑ์ด๋‚˜ ์“ฐ์ž„๋ฒ•์— ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค๊ณ  ๋ณด์•˜๋‹ค. ์—…๊ทธ๋ ˆ์ด๋“œ ๋ฒ„์ „์ด๋ผ๊ณ  ๋ณธ ๋“ฏ?

์•„๋ฌดํŠผ!

์ด๋ฏธ์ง€๋„ ์ €์žฅํ•  ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•ด์ค˜์•ผ ํ–ˆ๋Š”๋ฐ

/images/๊ฒŒ์‹œ๊ธ€id/์ด๋ฏธ์ง€id

์ด๋ ‡๊ฒŒ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.

firebase storage๋Š” ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด ์—…๋กœ๋“œ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ๊ทธ ๊ฒฝ๋กœ๋ฅผ ๋ฐ›์•„์„œ ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ์— ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ• ๋•Œ img ๊ฒฝ๋กœ์— ํ•ด๋‹น url์„ ๋„ฃ์–ด์ฃผ๋ฉด ์ด๋ฏธ์ง€๊ฐ€ ์กฐํšŒ๋œ๋‹ค.

2. ์ˆ˜์ •, ์‚ญ์ œ

๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์‹œ์—๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋ณ€๊ฒฝํ•˜์ง„ ๋ชปํ•˜๊ณ  ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ฒŒ๋งŒ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ์ด๋ถ€๋ถ„์€ ๋‚˜์ค‘์— ๋ณ€๊ฒฝ๋„ ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ฐ”๊ฟ€ ์˜ˆ์ •์ด๋‹ค.

๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •์ด๋‚˜ ์‚ญ์ œ ๊ณผ์ •๋„ ์ €์žฅ ์ž‘์—…๊ณผ ๋น„์Šทํ•˜๋‹ค. ๋จผ์ € ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๋‹ค๋ฃจ๊ณ  ๊ทธ ํ›„์— ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฌ๋‹ค.

๋‹ค๋งŒ ์‚ญ์ œ ํ• ๋•Œ๋Š” ๋ฌด์กฐ๊ฑด ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๋ฉด ๋˜์ง€๋งŒ ์ˆ˜์ •ํ• ๋•Œ๋Š” ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘˜ ๊ฒƒ์ธ์ง€ ์‚ญ์ œํ•  ๊ฒƒ์ธ์ง€ ๊ตฌ๋ถ„ํ•ด์„œ ๋™์ž‘ํ•ด์•ผ ํ•œ๋‹ค.

๋‚˜๋Š” ์ˆ˜์ •์ฐฝ์—์„œ ์ด๋ฏธ์ง€ ์‚ญ์ œ ๋ฒ„ํŠผ(X) ์„ ๋ˆ„๋ฅด๋ฉด isImageDel state๊ฐ’์„ true๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ๊ฒŒ์‹œ๊ธ€ ์—…๋ฐ์ดํŠธ action์— ํ•ด๋‹น ๊ฐ’์„ ์ „๋‹ฌํ•ด์„œ ๊ฐ’์ด true์ด๋ฉด ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ , false์ด๋ฉด ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋งŒ์•ฝ ์‚ญ์ œํ•œ๋‹ค๋ฉด ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ์˜ image, imageUrl ๋ฐ์ดํ„ฐ๋„ ๋นˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋˜๋„๋ก ๋น„์›Œ์ฃผ์—ˆ๋‹ค.

3. API ๋ถ„๋ฆฌ (๊ทผ๋ฐ ํด๋ฆฐ์ฝ”๋“œ๋ฅผ ๊ณ๋“ค์ธ)

์ง€๊ธˆ๊นŒ์ง€ ํ•˜๋‚˜์˜ firebase.js ํŒŒ์ผ์— ๋ชจ๋“  api ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ์ด๋Ÿฌ๋‹ค๋ณด๋‹ˆ ํŒŒ์ผ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์ง€๊ณ  ๊ฐ€๋…์„ฑ์ด ์•ˆ์ข‹์•„์ ธ์„œ realtime datababse API์™€ storage API๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

์‚ฌ์‹ค ์ด๊ฑด ์•„์ง ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

์ •ํ™•ํžˆ ๋งํ•˜์ž๋ฉด api ํŒŒ์ผ์€ ๋ถ„๋ฆฌํ–ˆ์ง€๋งŒ ์ฝ”๋“œ๊ฐ€ ๋งŽ์ด ๋”๋Ÿฝ๋‹ค...

๋ˆˆ์— ๋ณด์ด๋Š” ์ œ์ผ ํฐ ๋ฌธ์ œ๋Š” firebase configuration ์„ค์ •์ด ์ค‘๋ณต๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์ฒ˜์Œ์—๋Š” ์ด๊ฑธ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์˜ˆ์ „์— ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์—์„œ ํ–ˆ๋˜ ๋ฐฉ์‹์ธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํด๋ž˜์Šค์™€ ์ธ์Šคํ„ด์Šค๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•ด ๋ณด๋ ค๊ณ  ๊ฑด๋“œ๋ ค๋ดค๋Š”๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ์˜ค๋ž˜ ๊ฑธ๋ฆด ๊ฒƒ ๊ฐ™์€๋ฐ๋‹ค ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์‹œ๊ฐ„๊นŒ์ง€ ์‚ฌ๋ผ์งˆ ๊ฒƒ ๊ฐ™์•„ ์•„์ง์€ ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค. ๋‚˜์ค‘์— ๋ฐฐํฌ ํ›„ ๋ฆฌํŒฉํ† ๋ง ํ•˜๋ฉฐ ๊ธฐ๋Šฅ์„ ๊ฐœ์„ ํ•  ๋•Œ ์‹œ๋„ํ•ด ๋ณผ ์ƒ๊ฐ์ด๋‹ค.

์ผ๋‹จ ๊ฐœ์ธ์ ์ธ ์ƒ๊ฐ์œผ๋กœ๋Š” class๋‚˜ class extends๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์–ด์ฐŒ์ €์ฐŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค


๊ตฌํ˜„

firebas

// ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€ ์ €์žฅ
export const addPost = (post) => {
  set(ref(db, `/community/posts/${post.id}`), post);
};

// ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€ ๋“ค๊ณ ์˜ค๊ธฐ
export const getPost = async () => {
  const snapshot = await get(child(dbRef, "/community/posts"));
  if (snapshot.exists()) {
    return Object.values(snapshot.val());
  }
};

// ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •
export const updatePost = async (post) => {
  update(ref(db, `/community/posts/${post.id}`), post);
};

// ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
export const removePost = async (postId) => {
  remove(ref(db, `/community/posts/${postId}`));
};

upload

// ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
export const addFile = async (postId, file) => {
  const attachmentRef = ref(storage, `/images/${postId}/${file.id}`);
  await uploadString(attachmentRef, file.url, "data_url");
  return getDownloadURL(ref(storage, attachmentRef));
};

// ์ด๋ฏธ์ง€ ์‚ญ์ œ
export const removeFile = async (postId, imageUrl) => {
  const attachmentRef = ref(storage, `/images/${postId}/${imageUrl}`);
  deleteObject(attachmentRef);
};

NewPost

export default function NewPost({ onClose, isEdit, post }) {
  const { user } = useAuthContext();
  const [content, setContent] = useState("");
  const [fileDataUrl, setFileDataUrl] = useState("");
  const [imageUrl, setImageUrl] = useState();
  const [isImageDel, setIsImageDel] = useState(false);
  const dispatch = useDispatch();

  const onClickDel = () => {
    if (isEdit) {
      setIsImageDel(true);
    } else {
      setFileDataUrl("");
    }
  };

  const onChangeHandler = (e) => {
    const { value, files } = e.target;

    // ํŒŒ์ผ ์—…๋กœ๋“œ์— ํ•„์š”ํ•œ url(reader.result) ์–ป๊ธฐ
    if (files) {
      // ํŒŒ์ผ ์ œ๊ฑฐํ–ˆ์„๋•Œ ๋ฐฉ์–ด๋กœ์ง
      if (files[0]) {
        const reader = new FileReader();
        reader.readAsDataURL(files[0]);
        reader.onload = () => {
          setFileDataUrl(reader.result);
        };
      }
      return;
    }

    // ๊ธ€๋‚ด์šฉ
    setContent(value);
  };

  const onSubmitHandler = async (e) => {
    e.preventDefault();

    // ์ˆ˜์ •์ฐฝ์ผ๊ฒฝ์šฐ
    if (isEdit) {
      const newPost = {
        ...post,
        content,
      };

      dispatch(updatePostFetch(newPost, isImageDel));
      onClose();
      return;
    }

    const newPost = {
      id: uuidv4(),
      content,
      auth: user.email,
      commentCount: 0,
      createdAt: new Date().getTime(),
    };

    dispatch(addPostFetch(newPost, fileDataUrl));

    setContent("");
    setFileDataUrl("");
  };

  useEffect(() => {
    // ์ฒ˜์Œ ์ˆ˜์ • ํŽ˜์ด์ง€์— ๋“ค์–ด์™”์„ ๋•Œ ํŒŒ์ผ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ณด์—ฌ์ฃผ๊ณ  ํŒŒ์ผ ๋ฐ์ดํ„ฐ ์œ ์ง€
    if (isEdit) {
      setContent(post.content);
      setImageUrl(post.imageUrl);
    }
  }, [isEdit, post]);

  return (
    <>
      {user && (
        <div className={styles.newPost}>
          <p className={styles.user}>{user.email}</p>
          <article onSubmit={onSubmitHandler}>
            <textarea
              type="text"
              value={content || ""}
              onChange={onChangeHandler}
              placeholder="์ตœ๋Œ€ 300์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."
              maxLength="300"
              required
            />

            {!isImageDel && (fileDataUrl || imageUrl) && (
              <div className={styles.imgBox}>
                <img
                  src={imageUrl ? imageUrl : fileDataUrl && fileDataUrl}
                  alt="์ฒจ๋ถ€"
                />
                <span onClick={onClickDel}>
                  <TiDelete className={styles.delBtn} />
                </span>
              </div>
            )}
          </article>
          {!isEdit && (
            <footer className={styles.submitBox}>
              <label htmlFor="file">
                <BsCardImage />
              </label>
              <input
                type="file"
                id="file"
                accept="image/*"
                onChange={onChangeHandler}
                hidden
              />
              <Button text="์ž‘์„ฑ" onClick={onSubmitHandler} />
            </footer>
          )}

          {isEdit && (
            <footer>
              <Button text="์ˆ˜์ •" onClick={onSubmitHandler} />
            </footer>
          )}
        </div>
      )}
    </>
  );
}

DeletePost

export default function DeletePost({ onClose, post }) {
  const dispatch = useDispatch();

  const onRemoveHandler = () => {
    dispatch(removePostFetch(post));
    onClose();
  };

  return (
    <div className={styles.deletePost}>
      <h1>์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</h1>
      <div className={styles.buttonBox}>
        <Button text="์ทจ์†Œํ•˜๊ธฐ" onClick={() => onClose()} cancle />
        <Button text="์‚ญ์ œํ•˜๊ธฐ" onClick={onRemoveHandler} />
      </div>
    </div>
  );
}

PostCard

export default function PostCard({ post }) {
  const { user } = useAuthContext();
  const [showEdit, setShowEdit] = useState(false);
  const [showDelete, setShowDelete] = useState(false);

  const toggleShowEdit = () => {
    setShowEdit((showEdit) => !showEdit);
  };

  const toggleShowDelete = () => {
    setShowDelete((showDelete) => !showDelete);
  };

  return (
    <Card className={styles.postCard}>
      <div className={styles.headerBox}>
        <h1>{post.auth}</h1>
        <div>
          {user.email === post.auth && (
            <div className={styles.menu}>
              <BiDotsHorizontalRounded
                id="menuBtn"
                className={styles.menuIcon}
              />
              <Card className={styles.subMenu}>
                <ul>
                  <li onClick={toggleShowEdit}>์ˆ˜์ •ํ•˜๊ธฐ</li>
                  <li onClick={toggleShowDelete}>์‚ญ์ œํ•˜๊ธฐ</li>
                </ul>
              </Card>
            </div>
          )}
        </div>
      </div>

      <time
        className={styles.timeAgo}
        dateTime={new Date(parseInt(post.createAt))}>
        {post.createAt}
      </time>

      <div className={styles.content}>
        <p>{post.content}</p>
        {post.imageUrl && post.imageUrl.length > 0 && (
          // <Link to={post.imageUrl}>
          <a href={post.imageUrl} target="_blank" rel="noopener noreferrer">
            <img src={post.imageUrl} alt="์ฒจ๋ถ€" />
          </a>
          // </Link>
        )}
      </div>

      {showEdit && (
        <PostModal type={EDIT} toggleEdit={toggleShowEdit} post={post} />
      )}
      {showDelete && (
        <PostModal type={DELETE} toggleDelete={toggleShowDelete} post={post} />
      )}
    </Card>
  );
}

PostModal

const Backdrop = ({ toggleMenu }) => {
  return <div className={styles.backdrop} onClick={toggleMenu} />;
};

const ModalOverlay = ({ toggleEdit, toggleDelete, post, type }) => {
  if (type === EDIT) {
    return (
      <div className={`${styles.overlay} ${styles.edit}`}>
        <NewPost onClose={toggleEdit} post={post} isEdit />
      </div>
    );
  } else if (type === DELETE) {
    return (
      <div className={`${styles.overlay} ${styles.delete}`}>
        <DeletePost onClose={toggleDelete} post={post} />
      </div>
    );
  }
};
export default function PostModal({ toggleEdit, toggleDelete, post, type }) {
  // ํ˜„์žฌ ์œ„์น˜์—์„œ ๋„์šฐ๊ธฐ
  useEffect(() => {
    document.body.style.cssText = `
    top: -${window.scrollY}px;
    overflow-y: scroll;
    width: 100%;`;
    return () => {
      const scrollY = document.body.style.top;
      document.body.style.cssText = "";
      window.scrollTo(0, parseInt(scrollY || "0", 10) * -1);
    };
  }, []);

  return (
    <div>
      {ReactDOM.createPortal(
        <Backdrop toggleMenu={toggleEdit || toggleDelete} />,
        document.getElementById("backdrop-root")
      )}
      {ReactDOM.createPortal(
        <ModalOverlay
          toggleEdit={toggleEdit}
          toggleDelete={toggleDelete}
          post={post}
          type={type}
        />,
        document.getElementById("modal-root")
      )}
    </div>
  );
}

๊ตฌํ˜„ํ™”๋ฉด

  • ์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฉ”์ธ ํŽ˜์ด์ง€
    ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๊ธ€๋งŒ ... ์•„์ด์ฝ˜์ด ๋‚˜ํƒ€๋‚œ๋‹ค

  • ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •, ์‚ญ์ œ๋Š” ๋ชจ๋‹ฌ์ฐฝ์œผ๋กœ ๋‚˜ํƒ€๋‚œ๋‹ค


ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

1. firebase Storage ๊ถŒํ•œ

๐Ÿšจ Uncaught (in promise) FirebaseError: Firebase Storage: User does not have permission to access 'images/c0a490be-a907-40a5-8571-84ce934d088d/fd60fcc8-6327-4ff6-9798-e68fb404db4e'. (storage/unauthorized)

firebase storage๋ฅผ writeํ•˜๋Š” ๊ถŒํ•œ์ด ์—†์–ด์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ

firestore > project > storage > Rulesํƒญ์—์„œ allow read, write: if false ๋ฅผ allow read, write: if true ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ํ•ด๊ฒฐ

2. redux state๊ฐ’์ด Proxy๋กœ ๋‚˜์˜ค๋Š” ๋ฌธ์ œ

๋ฆฌ๋•์Šค add์•ก์…˜ ๋•Œ cannot read properties of undefined (reading โ€˜pushโ€™)๊ฐ€ ์—๋Ÿฌ๊ฐ€ ๋– ์„œ state๊ฐ’์„ ํ™•์ธํ•ด๋ณด๋‹ˆ

์ด๋ ‡๊ฒŒ Proxy๋กœ ๋‚˜์˜ค๋Š”์ค‘

current(state) ์ฒ˜๋ฆฌ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค

profile
๋” ์ด์ƒ ๋ฏธ๋ฃฐ ์ˆ˜ ์—†๋‹ค

0๊ฐœ์˜ ๋Œ“๊ธ€