React ๋ฅผ ์ด์ฉํ์ฌ ์์
ํ ๋ช
ํจ ์ ์ ์น ์ดํ๋ฆฌ์ผ์ด์
์
๋๋ค.
Firebase ์ Authentication ์๋น์ค๋ฅผ ์ด์ฉํ์ฌ ์์
๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ์๊ณ ,
์ด ๊ณผ์ ์์ ์ป์ด์จ user uid ๊ฐ์ผ๋ก Realtime database ์ ๋ช
ํจ์ ์ค์๊ฐ์ผ๋ก CRUD ํ ์ ์๊ฒ๋ ๊ตฌํ ํ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Cloudinary ๋ฅผ ์ด์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์๋ฒ์ ์
๋ก๋ ํ ์ ์๊ฒ ํ์ฌ ์ธ์ ์ด๋์๋ ํด๋น ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
๋ํ ์ธ๋ถ๋ผ์ด๋ธ๋ฌ๋ฆฌ(dom-to-image, fileSaver) ๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑํ ์นด๋๋ฅผ png ํ์ผ๋ก ์ ์ฅํ ์ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํ์์ต๋๋ค.
Live Demo : Business Card Maker
์์ ๊ธฐ๊ฐ (2022.06.09 ~ 2022.06.13)
- React
- React Functional Component
- React Router
- PostCSS
- Firebase
- Authentication
- Realtime Database
- Cloudinary
- Deploy: Netlify
syncCard()
ํจ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํด๋น ref(๊ฒฝ๋ก) ์ ๋ฆฌ์ค๋๋ฅผ ๋ฑ๋กํ์ฌ ๋ณ๊ฒฝ์ฌํญ์ด ์๋ค๋ฉด ๋ฑ๋ก๋ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ถํ์ ๋ฆฌ์ค๋๋ฅผ ์ ๊ฑฐํด์ฃผ๋ off()
ํจ์๋ฅผ ๋ฆฌํดํด์ค๋๋ค.
์ด๋ maker ์ปดํฌ๋ํธ์ useEffect()
์์์ ์ฝ๋ฐฑํจ์ํํ๋ก ๋ฆฌํด์ ํด์ฃผ์ด ์ธ๋ง์ดํธ๊ฐ ๋๋ฉด off()
ํจ์๊ฐ ์คํ๋ฉ๋๋ค.
// card_database.js
import { firebaseDatabase } from './firebase';
class CardDatabase {
//..์๋ต
syncCard(userId, onUpdate) {
const ref = firebaseDatabase.ref(`${userId}/cards`);
ref.on('value', (snapshot) => {
const value = snapshot.val();
value && onUpdate(value);
});
return () => ref.off();
}
}
export default CardDatabase;
// maker.js
useEffect(() => {
if (!userId) {
return;
}
const stopSync = cardDatabase.syncCard(userId, (value) => {
setCards(value);
});
return () => stopSync();
}, [userId, cardDatabase]);
file ์ ์ธ์๋ก ๋ฐ์์ ์๋ฒ์ ์ ๋ก๋ ํ ํด๋น ์ด๋ฏธ์ง url ์ ๋ฆฌํดํด์ค๋๋ค.
class UploadImage {
async upload(file) {
const data = new FormData();
data.append('file', file);
data.append('upload_preset', 'docs_upload_example_us_preset');
const result = await fetch(
'https://api.cloudinary.com/v1_1/demo/image/upload',
{
method: 'POST',
body: data,
}
);
return result.json();
}
}
export default UploadImage;
toBlob()
ํจ์๋ฅผ ํตํด ์ด๋ฒคํธ๊ฐ ์ผ์ด๋ ํ๊ฒ์(card) ์ ํํ๊ณ saveAs()
ํจ์๋ก ํด๋น ํ๊ฒ์ png ํ์ผ๋ก ์ ์ฅ์์ผ ์ค๋๋ค.
import React, { memo, useRef } from 'react';
import styles from './card.module.css';
import domToImage from 'dom-to-image';
import { saveAs } from 'file-saver';
const Card = memo(({ card }) => {
const cardRef = useRef();
const onSaveFile = () => {
const card = cardRef.current;
// dom-to-image
const filter = (card) => {
return card.tagName !== 'BUTTON';
};
domToImage.toBlob(card, { filter: filter }).then((blob) => {
// fileSaver
saveAs(blob, `๐ผ ${name} card.png`);
});
};
return (
<li className={styles.list}>
<div ref={cardRef}>
<article className={`${styles.card} ${getTheme(theme)}`}>
<button className={styles.save} onClick={onSaveFile} title="save">
<img src="./images/save.png" alt="save" />
</button>
//... ์๋ต
</article>
</div>
</li>
);
});
export default Card;
search componenet ์ input value๋ฅผ setSearch()
์ ์ ์ฅํ๊ณ , search ์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์ค๋ฉด card ์ name ๊ณผ ๋น๊ตํ์ฌ ํด๋นํ๋ card ๋ฅผ ๋ฆฌํดํด์ฃผ๊ณ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด card ์ ์ฒด๋ฅผ ๋ฆฌํดํฉ๋๋ค.
import React, { useState } from 'react';
import Card from '../card/card';
import Search from '../search/search';
import styles from './preview.module.css';
const Preview = ({ cards }) => {
const [search, setSearch] = useState('');
const onChange = (e) => {
setSearch(e.target.value);
};
return (
<section className={styles.preview}>
<div className={styles.search}>
<Search onChange={onChange} />
</div>
<ul>
{!search &&
Object.keys(cards).map((key) => <Card key={key} card={cards[key]} />)}
{search &&
Object.keys(cards)
.filter((key) => cards[key].name.toLowerCase().includes(search))
.map((key) => <Card key={key} card={cards[key]} />)}
</ul>
</section>
);
};
export default Preview;