fetch API 써보기

나혜수·2023년 2월 28일
0

자바스크립트 실전

목록 보기
12/19

🔖 목표 : 상품 정보와 상품 옵션을 보여주고 옵션을 선택할 수 있는 컴포넌트를 만들어본다.


API

API_END_POINT = https://misc.edu-api.programmers.co.kr/

상품 정보 조회 API : https://misc.edu-api.programmers.co.kr/products?id=1
url : products?id=1

[
  {"id":1,
   "name":"프로그래머스 마우스 패드",
   "basePrice":10000,"published_at":"2023-02-14T09:57:00.586Z",
   "created_at":"2023-02-14T09:29:26.262Z",
   "updated_at":"2023-02-14T09:57:00.594Z"}
]

상품 옵션 조회 API : https://misc.edu-api.programmers.co.kr/product-options?product.id=1
url : product-options?product.id=1

[
    {
        "id": 1,
        "optionName": "기본형",
        "optionPrice": 0,
        "created_at": "2023-02-14T09:30:17.949Z",
        "updated_at": "2023-02-14T09:31:08.029Z"
    },
    {
        "id": 2,
        "optionName": "무선충전기 일체형",
        "optionPrice": 15000,
        "created_at": "2023-02-14T09:31:30.086Z",
        "updated_at": "2023-02-14T09:31:30.090Z"
    },
    {
        "id": 3,
        "optionName": "완전방수처리형",
        "optionPrice": 3000,
        "created_at": "2023-02-14T09:32:29.417Z",
        "updated_at": "2023-02-14T09:32:29.422Z"
    }
]

상품 옵션 수량 조회 API : https://misc.edu-api.programmers.co.kr/product-option-stocks?productOption.id=1
url : product-option-stocks?productOption.id=1

[
  {"id":1,
   "stock":24,
   "created_at":"2023-02-14T09:34:05.545Z",
   "updated_at":"2023-02-14T09:34:05.549Z"}
]

1. 더미 데이터로 값을 잘 받아오는지 확인하기

main.js

import ProductOptions from "./productOptions.js"

const dummy_data = [
    {
        id: 1,
        optionName: "기본형",
        optionPrice: 0,
        stock : 10
    },
    {
        id: 2,
        optionName: "나방형",
        optionPrice: 0,
        stock : 10
    },
    {
        id: 3,
        optionName: "응용형",
        optionPrice: 0,
        stock : 0
    },

]

const $App = document.querySelector("#App")

new ProductOptions({
    $target : $App,
    initialState : dummy_data,
    onSelect : (option) => {alert(option.optionName)}
})

productOptions.js

export default function ProductOptions({$target,initialState,onSelect}){
    const $select = document.createElement('select')
    $target.appendChild($select)

    // 상품옵션 이름 렌더링 시 [상품명 + 옵션명 + 재고 : n개] 이런 형식으로 보여줘야 함
    // 재고가 0인 상품의 경우 옵션을 선택하지 못하게 함 

    this.state = initialState

    this.setState = (nextState) =>{
        this.state = nextState
        this.render()
    }

    const createOptionFullName = ({optionName, optionPrice, stock}) => {
        return `${optionName} 
                ${optionPrice > 0 ? `(옵션가 : ${optionPrice}` : ''} | 
                ${stock > 0 ? `재고 : ${stock}` :'재고 없음'}
                `
    }

    $select.addEventListener('change',(e)=>{
        const optionId = parseInt(e.target.value)
        const option = this.state.find(option => option.id === optionId)
        onSelect(option)
    })

    // 기본적으로 render 함수는 파라미터가 없어야 한다. 순수하게 state만을 보고 렌더링되어야 한다. 
    this.render = () => {
        if(this.state && Array.isArray(this.state)){
            // this.state가 제대로 들어있는지, 배열인지 아닌지 판별
            $select.innerHTML = `
                <option value='' disabled selected>선택하세요</option>
                ${this.state.map((option)=>
                                                // <option disabled>
                 `<option value="${option.id}" ${option.stock === 0 ? 'disabled' : ''}>
                  ${createOptionFullName(option)}</option>"`).join('')}           
            `
        }
    }

    this.render()
}


2. API 붙이기

main.js

import ProductOptions from "./productOptions.js"
import {request} from "./api.js"

const $App = document.querySelector("#App")

const fetchOptionData = (id) => {
    return request(`products?id=${id}`)
    .then(products => {
        return request(`product-options?product.id=${id}`)       
    })
    .then(productsOptions => {
        return Promise.all([
            Promise.resolve(productsOptions),
            Promise.all(
                productsOptions.map(productsOptions => productsOptions.id).map(id => {
                    return request(`product-option-stocks?productOption.id=${id}`)
                })
            )

        ])
    })
    .then(data => {
        const [options, stocks] = data // Stocks [][][]
        const optionData = options.map((options,i) =>{
            const stock = stocks[i][0].stock

            return {
                optionID : options.id,
                optionName : options.optionName,
                optionPrice : options.optionPrice,
                stock : stock
            }

        })
        productOptionsComponent.setState(optionData)
    })  
}

const defaultId = 1
fetchOptionData(defaultId)

const productOptionsComponent = new ProductOptions({
    $target : $App,
    initialState : [],
    onSelect : (option) => {alert(option.optionName)}
})

fetchOptionData 3번째 then의 data를 콘솔에 출력한 사진

api.js

/* 서버를 여러개쓰면 서버 앞단의 공통된 도메인을 따로 뽑아 상수로 관리하는 것이 좋다. */
const API_END_POINT = "https://misc.edu-api.programmers.co.kr"


export const request = (url) => {
    return fetch(`${API_END_POINT}/${url}`)
    .then(res => {
        if(res.ok){
            return res.json()
        }
        throw new Error(`${res.state} Error`)
    })
    .catch(e => alert(e.message))
}

productOptions.js

export default function ProductOptions({$target,initialState,onSelect}){
    const $select = document.createElement('select')
    $target.appendChild($select)

    // 상품옵션 이름 렌더링 시 [상품명 + 옵션명 + 재고 : n개] 이런 형식으로 보여줘야 함
    // 재고가 0인 상품의 경우 옵션을 선택하지 못하게 함 

    this.state = initialState

    this.setState = (nextState) =>{
        this.state = nextState
        this.render()
    }

    const createOptionFullName = ({optionName, optionPrice, stock}) => {
        return `${optionName} 
                ${optionPrice > 0 ? `(옵션가 : ${optionPrice}` : '(옵션가 : 0'} | 
                ${stock > 0 ? `재고 : ${stock})` :'(재고 없음)'}
                `
    }

    $select.addEventListener('change',(e)=>{
        const optionId = parseInt(e.target.value)
        console.log(e.target)
        const option = this.state.find(option => option.optionID === optionId)
        onSelect(option)
    })

    // 기본적으로 render 함수는 파라미터가 없어야 한다. 순수하게 state만을 보고 렌더링되어야 한다. 

    this.render = () => {
        if(this.state && Array.isArray(this.state)){
            // this.state가 제대로 들어있는지, 배열인지 아닌지 판별
            $select.innerHTML = `
                <option value='' disabled selected>선택하세요</option>
                ${this.state.map((option)=>
                                                  // <option disabled>
                    `<option value="${option.optionID}" 
									${option.stock === 0 ? 'disabled' : ''}>
                        ${createOptionFullName(option)}
					</option>"`).join('')}           
            `
        }
    }

    this.render()
}


3. ProductPage.js로 옮기기

ProductPage.js

/* state 구조
    {
        productID: 1,      // 외부에서 받는 값

        // api로 불러오는 data 
        product: product,  
        optionData: []     //  2단계에서 만든 data
    }  
*/

import ProductOptions from "./productOptions.js"
import {request} from "./api.js"

export default function productPage({
    $target,
    initialState
}){
    const $product = document.createElement('div')
    $target.appendChild($product)

    this.state = initialState // initialState => { productID: 1 }

    // productOptionsComponent 컴포넌트 생성 
    const productOptionsComponent = new ProductOptions({
        $target : $product,
        initialState : [],
        onSelect : (option) => {alert(option.optionName)}
    })

    

    this.setState = (nextState) =>{
        /* nextState.productId와 현재 this.state.productId가 맞지 않는 경우는 
           fetchOptionData에 nextState.productId를 넘겨준다. */
        if(nextState.productID !== this.state.productID){
            fetchOptionData(nextState.productId)
            return 
        }
        this.state = nextState
        productOptionsComponent.setState(this.state.optionData)
        // optionData가 fetch로 새로 들어오면 productOptionsComponent 상태도 같이 업데이트 
        this.render()
    }

    this.render = () => {

    }
    this.render()


    // fetch 작업 
    const fetchOptionData = (id) => {
        return request(`products?id=${id}`)
        .then(products => {
            this.setState({
                ...this.state,
                products,
                optionData : []
            })
            // this.state : { productID: 1 } => { productID: 1, products, optionData : [] }
            // 여기서 products 값이 채워짐 

            
            return request(`product-options?product.id=${id}`)       
        })
        .then(productsOptions => {
            return Promise.all([
                Promise.resolve(productsOptions),
                Promise.all(
                    productsOptions.map(productsOptions => productsOptions.id).map(id => {
                        return request(`product-option-stocks?productOption.id=${id}`)
                    })
                )
    
            ])
        })
        .then(data => {
            const [options, stocks] = data // Stocks [][][]
            const optionData = options.map((options,i) =>{
                const stock = stocks[i][0].stock
    
                return {
                    optionID : options.id,
                    optionName : options.optionName,
                    optionPrice : options.optionPrice,
                    stock : stock
                }
    
            })

            this.setState({
                ...this.state,
                optionData

            // this.state의 optionData에 값을 넣어주는 작업 
            // 초기 id만 있던 상태에서 products, optinData를 모두 가진 this.state 완성!     
            })
        })  
    }

    fetchOptionData(this.state.productID) // 외부에서 받아온 id로 fetch 작업 실시 
}

main.js

import productPage from "./productPage.js"

const $App = document.querySelector("#App")

// productID만 외부에서 받아옴 
new productPage({
    $target:$App,
    initialState:{
        productID: 1,
    }
})

4. cart.js 만들기

cart.js

/* state 구조
    {
        productName: 상품 이름,      
        basePrice: 기본 가격,  
        selectedOption: [option] 
    }  
*/

export default function Cart({$target, initialState, onRemove}){
    const $cart = document.createElement('div')
    $target.appendChild($cart)

    this.state = initialState

    this.setState = (nextState) => {
        this.state = nextState
        this.render()
    }

    const calculateTotalPrice = () => {
        const {basePrice, selectedOption} =  this.state
        return selectedOption.reduce((acc, option) => 
            acc + (basePrice + option.optionPrice) * option.ea, 0)
    }

    this.render = () => {
        const {productName, basePrice, selectedOption} = this.state
        $cart.innerHTML = `
            <ul>
              ${Array.isArray(this.state.selectedOption) && 
          		this.state.selectedOption.map((option) => 
                  `<li>${productName} - ${option.optionName} | 
                  ${basePrice + option.optionPrice}, ${option.ea}개</li>`).join('') }
            </ul>
            <div>${calculateTotalPrice()}</div>`
    }

    this.render()

}

ProductPage.js

import Cart from "./cart.js"

export default function productPage({
    $target,
    initialState
}){
    const $product = document.createElement('div')
    $target.appendChild($product)

    this.state = initialState // initialState => { productID: 1 }

    // Cart 생성
    // 우선 더미 데이터로 렌더링이 잘 되는지 확인  
    const cart = new Cart({
        $target: $product,
        initialState:{
            productName: '상품 이름',      
            basePrice: 10000,  
            selectedOption: [
                {
                    optionName: '옵션1',
                    optionPrice: 1000,
                    ea : 1   
                },
                {
                    optionName: '옵션2',
                    optionPrice: 2000,
                    ea : 1   
                },
            ]
        },
        onRemove : () => {}
    })
    
    // 이하 생략 


5. ProductPage.js 완성

main.js

import productPage from "./productPage.js"

const $App = document.querySelector("#App")


new productPage({
    $target:$App,
    initialState:{
        productID: 1
    }
})

api.js

/* 서버를 여러개쓰면 서버 앞단의 공통된 도메인을 따로 뽑아 상수로 관리하는 것이 좋다. */
const API_END_POINT = "https://misc.edu-api.programmers.co.kr"


export const request = (url) => {
    return fetch(`${API_END_POINT}/${url}`)
    .then(res => {
        if(res.ok){
            return res.json()
        }
        throw new Error(`${res.state} Error`)
    })
    .catch(e => alert(e.message))
}

ProductPage.js

/* state 구조
    {
        productID: 1,      // 외부에서 받는 값

        // api로 불러오는 data 
        product: product,  
        optionData: []     //  2단계에서 만든 data
        selectedOptions : 
    }  
*/

import ProductOptions from "./productOptions.js"
import {request} from "./api.js"
import Cart from "./cart.js"

export default function productPage({
    $target,
    initialState
}){
    const $product = document.createElement('div')
    $target.appendChild($product)

    this.state = initialState // initialState => { productID: 1 }

    // productOptionsComponent 컴포넌트 생성 
    const productOptionsComponent = new ProductOptions({
        $target : $product,
        initialState : [],
        onSelect : (option) => {
            const nextState = {...this.state}

            const {selectedOptions} = nextState
            
            const selectedOptionIndex = selectedOptions.findIndex((selectedOption) => 
                  		selectedOption.optionID === option.optionID)

            if (selectedOptionIndex > -1){
                nextState.selectedOptions[selectedOptionIndex].ea++
            }else{
                nextState.selectedOptions.push({
                    optionID: option.optionID,
                    optionName: option.optionName,
                    optionPrice: option.optionPrice,
                    ea : 1
                })
            }
            this.setState(nextState)
        }
    })

    // Cart 생성
    const cart = new Cart({
        $target: $product,
        initialState:{
            productName: '',      
            basePrice: 0,  
            selectedOptions: [ ]
        },
        onRemove : (selectedOptionIndex) => {
            const nextState = {...this.state}
            nextState.selectedOptions.splice(selectedOptionIndex,1)
            this.setState(nextState)
        }
    })

    this.setState = (nextState) =>{
        /* nextState.productId와 현재 this.state.productId가 맞지 않는 경우는 
           fetchOptionData에 nextState.productId를 넘겨준다. */
        if(nextState.productID !== this.state.productID){
            fetchOptionData(nextState.productId)
            return 
        }

        this.state = nextState

        const {optionData,products,selectedOptions} = this.state

        productOptionsComponent.setState(optionData)
        // optionData가 fetch로 새로 들어오면 productOptionsComponent 상태도 같이 업데이트 

        /* productOptionsComponent에서 onSelect가 실행되면 
           this.state(nextState)가 실행되면서 cart 상태를 업데이트 */
        cart.setState({
            productName: products[0].name,
            basePrice: products[0].basePrice,
            selectedOptions: selectedOptions
        })

        this.render()
    }

    this.render = () => {

    }
    this.render()


    // fetch 작업 
    const fetchOptionData = (id) => {
        return request(`products?id=${id}`)
        .then(products => {
            this.setState({
                ...this.state,
                products,
                optionData : [],
                selectedOptions : []
            })
            // this.state : { productID: 1 } => { productID: 1, products, optionData : [] }
            // 여기서 products 값이 채워짐 

            return request(`product-options?product.id=${id}`)       
        })
        .then(productsOptions => {
            return Promise.all([
                Promise.resolve(productsOptions),
                Promise.all(
                    productsOptions.map(productsOptions => productsOptions.id).map(id => {
                        return request(`product-option-stocks?productOption.id=${id}`)
                    })
                )
            ])
        })
        .then(data => {
            const [options, stocks] = data // Stocks [][][]
            const optionData = options.map((options,i) =>{
                const stock = stocks[i][0].stock
    
                return {
                    optionID : options.id,
                    optionName : options.optionName,
                    optionPrice : options.optionPrice,
                    stock : stock
                }
    
            })

            this.setState({
                ...this.state,
                optionData   

            // this.state의 optionData에 값을 넣어주는 작업 
            // 초기 id만 있던 상태에서 products, optinData를 모두 가진 this.state 완성!     
            })
        })  
    }
    
    fetchOptionData(this.state.productID) // 외부에서 받아온 id로 fetch 작업 실시

}

productOptions.js

export default function ProductOptions({$target,initialState,onSelect}){
    const $select = document.createElement('select')
    $target.appendChild($select)

    // 상품옵션 이름 렌더링 시 [상품명 + 옵션명 + 재고 : n개] 이런 형식으로 보여줘야 함
    // 재고가 0인 상품의 경우 옵션을 선택하지 못하게 함 

    this.state = initialState

    this.setState = (nextState) =>{
        this.state = nextState
        this.render()
    }

    const createOptionFullName = ({optionName, optionPrice, stock}) => {
        return `${optionName} 
                ${optionPrice > 0 ? `(옵션가 : ${optionPrice}` : '(옵션가 : 0'} | 
                ${stock > 0 ? `재고 : ${stock})` :'(재고 없음)'}
                `
    }

    $select.addEventListener('change',(e)=>{
        const optionId = parseInt(e.target.value)
        const option = this.state.find(option => option.optionID === optionId)
        onSelect(option)
    })

    // 기본적으로 render 함수는 파라미터가 없어야 한다. 순수하게 state만을 보고 렌더링되어야 한다. 

    this.render = () => {
        if(this.state && Array.isArray(this.state)){
            // this.state가 제대로 들어있는지, 배열인지 아닌지 판별
            $select.innerHTML = `
                <option value='' disabled selected>선택하세요</option>
                ${this.state.map((option)=>
                                                  // <option disabled>
                    `<option value="${option.optionID}" ${option.stock === 0 ? 'disabled' : ''}>
                        ${createOptionFullName(option)}</option>"`).join('')}           
            `
        }
    }
    this.render()
}

cart.js

/* state 구조
    {
        productName: 상품 이름,      
        basePrice: 기본 가격,  
        selectedOption: [option] 
    }  
*/

export default function Cart({$target, initialState, onRemove}){
    const $cart = document.createElement('div')
    $target.appendChild($cart)

    this.state = initialState

    this.setState = (nextState) => {
        this.state = nextState
        this.render()
    }

    const calculateTotalPrice = () => {
        const {basePrice, selectedOptions} =  this.state
        return selectedOptions.reduce((acc, option) => 
            acc + (basePrice + option.optionPrice) * option.ea, 0)
    }

    this.render = () => {
        const {productName, basePrice, selectedOptions} = this.state
        $cart.innerHTML = `
            <ul>
              ${Array.isArray(selectedOptions) && selectedOptions.map((option,i) => 
                `<li data-index="${i}" class='cartItem'>${productName} - ${option.optionName} | 
                ${basePrice + option.optionPrice}, ${option.ea}개
                <button class='remove'> x </button>
                </li>`).join('') }
            </ul>
            <div>${calculateTotalPrice()}</div>`
        
        $cart.querySelectorAll('.remove').forEach($button=>{
            $button.addEventListener('click',(e)=>{
                const $li = e.target.closest('li') // 버튼에서 가장 인접한 li 찾기
                const { index } = $li.dataset
                onRemove(parseInt(index))
            })
        })    
    }

    this.render()

}

profile
오늘도 신나개 🐶

0개의 댓글