Skip to content

YukJiSoo/react-practice-card-list

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

46 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

React Practice: Card List

๐ŸŒˆ ์‹คํ–‰ ๋ฐฉ๋ฒ•

1. yarn

2. yarn create:data   # dummy data jsonํŒŒ์ผ๋กœ ์ƒ์„ฑ

3. yarn start

๐ŸŒˆ ๊ตฌ์กฐ

ํŒŒ์ผ์˜ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

โ”œโ”€โ”€ actions
|
โ”œโ”€โ”€ components
|    โ”œโ”€โ”€ Card
|    โ”œโ”€โ”€ GlobalStyle
|    โ”œโ”€โ”€ OptionList
|    โ”œโ”€โ”€ TabList
|    โ””โ”€โ”€ Wish
|
โ”œโ”€โ”€ containers
|    โ””โ”€โ”€ Products
|         โ”œโ”€โ”€ Header
|         โ””โ”€โ”€ Main
|
โ”œโ”€โ”€ data # ๋”๋ฏธ ๋ฐ์ดํ„ฐ json ํŒŒ์ผ
|
โ”œโ”€โ”€ hooks
|
โ”œโ”€โ”€ pages
|    โ””โ”€โ”€ Products
|
โ”œโ”€โ”€ store
|    โ””โ”€โ”€ Product
|         โ”œโ”€โ”€ index.js
|         โ””โ”€โ”€ reducer.js
|
โ”œโ”€โ”€ utils
|
โ”œโ”€โ”€ App.js
|
โ””โ”€โ”€ index.js

โ“ UI ๊ตฌ์กฐ

UI๊ตฌํ˜„์— ์žˆ์–ด์„œ ๊ทœ์น™์„ ์ •ํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. ์—ญํ• ์„ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ํ•˜๋‚˜์˜ ํŒŒ์ผ์ด ๋น„๋Œ€ํ•ด์ง€๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ฝ”๋”ฉ๋ฐฉ์‹์— ๊ทœ์น™์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€์—์„œ ๋‹ด๋‹นํ•˜๋Š” ์—ญํ• ์„ Page, Container, Component๋กœ ๊ตฌ๋ถ„ ํ•˜๊ณ  ๊ฐ๊ฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ Page

๋ง๊ทธ๋Œ€๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€์—์„œ ์ „์ฒด์ ์ธ UI์˜ ๊ตฌ์กฐ๋ฅผ ํฌํ•จํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ Container๋‚˜ Component๋ฅผ ์กฐํ•ฉํ•ด์„œ Page๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ Container

Page๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ์กฐ๊ฐ์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ Container๋‚˜ Component๋ฅผ ์กฐํ•ฉํ•ด์„œ Container๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ Component

UI๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐ€์žฅ ์ž‘์€ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ Component๋ฅผ ์กฐํ•ฉํ•ด์„œ Component๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โ“ ์ƒํƒœ๊ด€๋ฆฌ

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉ๋œ Product context์˜ state๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
    selectedTab: DEFAULT_TAB,
    tabList: [
        { name: '์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ', lastTopPosition: 0 },
        { name: '์œ„์‹œ ๋ฆฌ์ŠคํŠธ', lastTopPosition: 0 },
    ],
    selectedSortOption: DEFAULT_OPTION,
    sortOptionList: [
        { name: '์ •๋ ฌ ์—†์Œ', method: defaultSort },
        { name: '๋†’์€ ๊ฐ€๊ฒฉ ์ˆœ์„œ', method: descendingSort },
        { name: '๋‚ฎ์€ ๊ฐ€๊ฒฉ ์ˆœ์„œ', method: ascendingSort },
    ],
    fetchedProducts: [ { id, name, price, thumbnailPath } ],
    wishList: { [id]: { id, name, price, thumbnailPath } }
};

wishList๋Š” local storage์— ์ €์žฅํ•˜์—ฌ ๊ด€๋ฆฌ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€ ํ†ต์‹ ์„ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ,

์ถ”๊ฐ€/์ œ๊ฑฐ๊ฐ€ ๋นˆ๋ฒˆํ•ด์ง€๋ฉด ์„œ๋ฒ„์— ๋ถ€ํ•˜๊ฐ€ ๋งŽ์„ ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ–ˆ๊ณ 

๊ฐœ์ธ์ด ์„ ํ˜ธํ•˜๋Š” ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ถ„์„์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด DB์—์„œ ๊ด€๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์ƒํƒœ๊ด€๋ฆฌ ๋˜ํ•œ UI ๊ตฌํ˜„์— ์ƒ๊ฐํ–ˆ๋˜ ๊ฒƒ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์—ญํ• ์„ ๋‚˜๋ˆ„๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. react์˜ contextAPI์™€ hooks๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€๊ณ  ํฌ๊ฒŒ action, store, reducer, dispatch๊ฐ€ ์ƒํƒœ๊ด€๋ฆฌ ์—ญํ• ์„ ๋ถ„๋‹ดํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ Store

import Reducer from './reducer';

const Context = createContext();

const initialValue = {}; // ํ•ด๋‹น state์—์„œ ๊ด€๋ฆฌํ• 

const Store = ({ children }) => {
    const [state, dispatch] = useReducer(Reducer, initialValue);

    return (
        <Context.Provider value={{ state, dispatchProduct }}>
            {children}
        </Context.Provider>
    );
};

store๋Š” ์œ„์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ state์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก context๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. store์—์„œ ๊ด€๋ฆฌํ•˜๋Š” state์— ๋Œ€ํ•œ ์กฐ์ž‘์„ ์ •์˜ํ•œ reducer๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ๋œ dispatch๋„ context๋ฅผ ํ†ตํ•ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ Reducer

const firstFunction = () => { ... }
const secondFunction = () => { ... }

const Reducer = (state, { type, payload }) => {
    const reducers = {
        [Actions.firstFunction]: firstFunction,
        [Actions.secondFunction]: secondFunction,
    };

    const reducer = reducers[type];
    return reducer ? reducer(state, payload) : state;
};

reducer๋Š” dispatch ํ•จ์ˆ˜์—์„œ ์ธ์ž๋กœ ๋„˜๊ฒจ์ค€ action์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. action๏ฟฝ์˜ type์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ณ  ์ด์— ๋”ฐ๋ผ state๊ฐ€ ์—…๋ฐ์ดํŠธ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ Action

const firstAction = 'firstAction';

const firstActionCreator = payload => {
    return { type: firstAction, payload };
};

dispatch ํ•จ์ˆ˜์—์„œ ์‚ฌ์šฉ๋  action์€ ActionCreator๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ Dispatch

const firstAction = firstActionCreator(payload);
dispatch(firstAction);

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


์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

1. Store์—์„œ state์™€ dispatch ํ•จ์ˆ˜๋ฅผ context๋ฅผ ํ†ตํ•ด ์ œ๊ณต
2. ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ context๋ฅผ ํ†ตํ•ด state๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉ๊ฐ€๋Šฅ
3. state๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์ ํ•ฉํ•œ action์„ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฅผ dispatch๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœํ•จ
4. reducer์—์„œ ๋ช…์‹œ๋œ action์˜ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ state ๊ด€๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋จ
5. ํ•ด๋‹น state์™€ ๊ด€๋ จ๋˜์–ด ์žˆ๋˜ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚จ

๐ŸŒˆ Infinite Scroll / Lazy Loading

Intersection Obeserver API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๊ธฐ๋Šฅ ๋ชจ๋‘ ํ™”๋ฉด์— ํŠน์ • DOM element๊ฐ€ ๋ณด์ด๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๊ฐ์ง€ํ•œ ํ›„ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” scroll๋˜๋Š” ์ˆœ๊ฐ„ ํ™”๋ฉด์— ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ง€๋Š”์ง€๊ฐ€ ๊ตฌํ˜„์— ๊ด€๊ฑด์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

scroll event๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, scroll๋  ๋•Œ ๋งˆ๋‹ค ๊ณผ๋„ํ•œ event๊ฐ€ ํ˜ธ์ถœ๋  ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ element์˜ ํฌ๊ธฐ์™€ ์œ„์น˜๊ฐ’์„ ์•Œ์•„๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” getBoundingClientRectํ•จ์ˆ˜๋Š” ํ˜ธ์ถœ ๋  ๋•Œ๋งˆ๋‹ค ๋ฆฌํ”Œ๋กœ์šฐ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ์„ฑ๋Šฅ์— ์ข‹์ง€ ์•Š๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

Intersection Obeserver API๋Š” DOM elemetn๊ฐ€ ๋ทฐํฌํŠธ์— ๋…ธ์ถœ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์— ํฐ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์•„ ์„ฑ๋Šฅ์ƒ ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

React์—์„œ Intersection Obeserver API๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด custom hooks๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

const useIntersectionObserver = ({
    root = null,
    rootMargin = '0px',
    threshold = 1,
    handleIntersection,
}) => {
    const [target, setTarget] = useState(null);

    useEffect(() => {
        if (!target) return;

        const option = { root, rootMargin, threshold };
        const io = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                entry.isIntersecting &&
                    handleIntersection(entry.target, observer);
            });
        }, option);

        io.observe(target);

        return () => io.unobserve(target);
    }, [target, root, rootMargin, threshold, handleIntersection]);

    return setTarget;
};

๊ฐ์‹œํ•˜๊ณ ์ž ํ•˜๋Š” element์— ์ ์šฉํ•  option๊ณผ ์ฝœ๋ฐฑํ•จ์ˆ˜๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์คŒ์œผ๋กœ์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜๋˜๋Š” setTarget ํ•จ์ˆ˜๋กœ ์›ํ•˜๋Š” element๋ฅผ Intersection Obeserver์— ๋“ฑ๋กํ•ด์ค๋‹ˆ๋‹ค. ๋ทฐํฌํŠธ์™€ target element๊ฐ€ ์ง€์ •ํ•œ ์„ค์ •๋Œ€๋กœ ๊ต์ฐจ๋˜๋ฉด ์ธ์ž๋กœ ๋„˜๊ฒจ์ค€ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ํ•œ hooks๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

const Card = () => {
    ...

    const setTarget = useIntersectionObserver({
        rootMargin: '5%',
        handleIntersection,
    });

    function handleIntersection(target, observer) {
        target.src = target.dataset.src;
        observer.unobserve(target);
    }

    useEffect(() => {
        setTarget(imgRef.current);
    }, [setTarget, imgRef]);

    ...
}

About

Make card list page wiht React

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published