프론트엔드/React 연습과제 해답

리액트 기초이론 9 - 리액트 훅(React Hook) 해답

syleemomo 2022. 3. 15. 17:25
728x90

 

* 연습과제 0

import './App.css';
import Modal from './Modal';
import Button from './Button';
import { useState } from 'react';

function App() {
  const [open, setOpen] = useState(false)
  
  const openModal = () => {
    setOpen(true)
  }
  const closeModal = () => {
    setOpen(false)
  }
  
  return (
    <div className="App">
      <Button handleClick={openModal}>Add Todo</Button>
      <Modal open={open}>
        <div className="header">You want to add new todo ?</div>
        <div className="body">
          <label>todo name: <input type="text"></input></label><br/>
          <label>todo description: <input type="text"></input></label>
        </div>
        <div className="footer">
          <Button size="small">Add</Button>
          <Button size="small" handleClick={closeModal}>Close</Button>
        </div>
      </Modal>
    </div>
  );
  
}

export default App;

 

* 연습과제 1

import './App.css';
import React, { useState } from 'react'

function App () {
  const [count, setCount] = useState(0)

  const showUI = (cnt) => {
    let ui = null;
    switch(cnt){
      case 0:
        ui = <h1>Home</h1>
        break;
      case 1:
        ui = <h1>About</h1>
        break;
      case 2:
        ui = <h1>Detail</h1>
        break;
      default:
        ui = <h1>NotFound</h1>
    }
    return ui
  }
  const increase = () => {
    setCount(count + 1)
  }
  
  return (
    <>
    {showUI(count)}
    <button type="button" onClick={increase}>카운팅</button>
    </>
  )
}

export default App;

 

* 연습과제 2

해답 1

import './App.css';
import React, { useEffect, useState } from 'react';
import animals from './dummyData'


function App() {
  const [count, setCount] = useState(0);

  const increaseCount = () => {
    setCount(count + 1)
  }
  useEffect( () => {
    const timerID = setInterval(increaseCount, 1000)
    return () => {
      clearInterval(timerID)
    }
  })
  const animal = animals[count%animals.length]

  return (
    <div className="App">
      <h1>Image Gallery !</h1> 
      <img src={animal.src} alt={animal.title}></img>
    </div>
  );
  
}

export default App;

동작은 하지만 useEffect 함수를 계속 새로 실행하면서 setInterval 함수가 타이머를 계속 다시 등록한다. 이는 불필요한 동작이다. 

 

해답 2

import './App.css';
import React, { useEffect, useState } from 'react';
import animals from './dummyData'


function App() {
  const [count, setCount] = useState(0);

  useEffect( () => {
    console.log('실행 -------> ')
    const increaseCount = () => {
      setCount(count => count + 1) // 함수형 업데이트 
    }
    const timerID = setInterval(increaseCount, 1000)
    return () => {
      clearInterval(timerID)
    }
  }, [])
  const animal = animals[count%animals.length]

  return (
    <div className="App">
      <h1>Image Gallery !</h1> 
      <img src={animal.src} alt={animal.title}></img>
    </div>
  );
  
}

export default App;

useEffect 함수의 두번째 인자로 빈 배열을 설정하였으므로 useEffect 함수는 초기 렌더링시에 한번만 실행된다. 하지만 이렇게 하면 setInterval 함수가 타이머를 등록하여 1초마다 increaseCount 함수가 실행되더라도 count 상태는 계속 초기값 0 에서 1로 변경되는 것이 반복된다. 즉, count 상태가 초기값으로 고정된다. 이를 해결하여 useEffect 함수를 한번만 실행하더라도 count 상태를 최신값으로 사용하기 위해서는 setCount 함수 내부에 콜백 형태로 설정해주면 된다. 이를 함수형 업데이트라고 한다. 

 

해답 3

import './App.css';
import React, { useEffect, useState } from 'react';
import animals from './dummyData'

function App() {
  const [count, setCount] = useState(0);

  useEffect( () => {
    const increaseCount = () => { // increaseCount 함수는 외부변수 count 를 사용하므로 최신 count 값에 따라 함수를 재생성하기 위하여 useEffect 내부에 위치시킴
      setCount(count + 1)
    }
    const timerID = setInterval(increaseCount, 1000)
    return () => {
      clearInterval(timerID)
    }
  }, [count]) // count 상태가 변경될때마다 useEffect 함수를 재실행함 
  const animal = animals[count%animals.length]

  return (
    <div className="App">
      <h1>Image Gallery !</h1> 
      <img src={animal.src} alt={animal.title}></img>
    </div>
  );
  
}

export default App;

이번에는 useEffect 함수의 두번째 인자로 count 상태를 설정하였다. 이렇게 하면 count 상태가 변경될때마다 useEffect 함수는 재실행된다. 하지만 이렇게 하더라도 해답 1과 동일하게 setInterval 을 이용한 타이머는 계속 재등록된다. 불필요한 동작이다. 

 

해답 4

import './App.css';
import React, { useEffect, useState } from 'react';
import animals from './dummyData'

function App() {
  const [count, setCount] = useState(0);
  const increaseCount = () => {
    setCount(count => count + 1) // 함수형 업데이트 
  }

  useEffect( () => {
    const timerID = setInterval(increaseCount, 1000)
    return () => {
      clearInterval(timerID)
    }
  }, [])
  const animal = animals[count%animals.length]

  return (
    <div className="App">
      <h1>Image Gallery !</h1> 
      <img src={animal.src} alt={animal.title}></img>
    </div>
  );
  
}

export default App;

가장 깔끔한 해답이다. useEffect 함수의 두번째 인자로 빈 배열을 설정하였기 때문에 setInterval 함수에 의한 타이머는 한번만 등록된다. 또한, increaseCount 함수를 타이머에 한번만 등록하더라도 setCount 함수는 함수형 업데이트를 사용하므로 최신 count 값을 계속 주입해서 사용할 수 있다. 

 

* 연습과제 3

import './App.css';
import React, { useState, useEffect } from 'react';
import Movie from './Movie';

function App() {

  // API 서버에서 데이터를 가져오고 상태를 변경하는 로직
  const [loading, setLoading] = useState(true)
  const [movies, setMovies] = useState([])

  useEffect( () => {
    fetch('https://yts.mx/api/v2/list_movies.json?limit=12')
    .then( res => res.json())
    .then( result => {
      const {data: {movies}} = result
      console.log(movies)
      setLoading(false)
      setMovies(movies)
    })
  }, [])

  // 스타일 코드
  const style = {
    display: 'flex',
    flexWrap: 'wrap',
    justifyContent: 'center',
    alignItems: 'center',
    width: '60%',
    margin: '100px auto',
    textAlign: 'center'
  }
  const loadingStyle = {
    position: 'absolute', 
    left: '50%', 
    top:'50%', 
    transform: 'translate(-50%, -50%)', 
    fontSize: '2rem'
  }

  // 화면 렌더링
  if(loading){
    return (
      <div style={loadingStyle}>
        <h1>Loading ...</h1>
      </div>
    )
  }else{
    return (
      <div style={style}>
        {movies.map(movie => {
          return (
            <Movie 
              key={movie.id}
              title={movie.title}
              genres={movie.genres}
              cover={movie.medium_cover_image}
              summary={movie.summary}
            ></Movie>
          )
        })}
      </div>
    )
  }
}

export default App;

 

* 연습과제 4

import './App.css';
import React, { useState, useEffect } from 'react'

function App() {
  const [numbers, setNumbers] = useState('')
  
  const pickRandomNumber = (min, max) => { return Math.floor( Math.random() * (max-min+1) ) + min }

  const isDuplicated = (numbers, picked) => {
    return numbers.find(num => num === picked)
  }
  const getLottoNum = (numbers) => {
    // console.log("length: ", numbers)
    const picked = pickRandomNumber(1, 45)

    const duplicatedNum = isDuplicated(numbers, picked) // 중복체크
    if(duplicatedNum){
      console.log('duplicated ...', duplicatedNum)
      getLottoNum(numbers) // 로또배열에 랜덤으로 뽑은 숫자가 이미 존재하면 재귀적으로 다시 숫자를 뽑음 
    }else{
      numbers.push(picked)
    }
  }
  const showRandomNumber = () => {
    const numbers = [] // 로또번호 배열
    
    while(numbers.length < 6){
      getLottoNum(numbers)
    }

    setNumbers(numbers.join('  '))
  }
  useEffect( () => {
    const countID = setInterval(showRandomNumber, 1000) // 타이머 설정
    return () => {
      clearInterval(countID) // 타이머 해제
    }
  })
 
  
  return (
    <div className='App'>
      <h1>로또번호 자동 생성기</h1>
      <h1>{numbers}</h1>
    </div>
  )
 
 
 
}
  
export default App;

 

* 연습과제 5

import './App.css';
import React, { useState, useEffect } from 'react';
import Word from './Word'

function App() {
  const [words, setWords] = useState([])

  useEffect( () => {
    const BASE_URL = 'https://dictionary-search.herokuapp.com/api/words'
    fetch(BASE_URL, {
      headers: {
          "Content-Type": "application/json",
      }
    })
    .then( res => res.json())
    .then( data => {
          console.log(data)
          const {words}=data
          setWords(words)
    })
  }, [])
  
  return (
    <div className="App">
      {words.map( (word, id) => {
        return (
          <Word 
            key={id}
            r_link={word.r_link}
            r_word={word.r_word}
            r_hanja={word.r_hanja}
            r_des={word.r_des}
          ></Word>
        ) 
      })}
    </div>
  );
}


export default App;

 

* 연습과제 6

import './App.css'; import React, { useState } from 'react'; 
import wordsData from './dictionaryData' 
import Button from './Button' 

function App() { 
  const [words, setWords] = useState(wordsData)

  const handleRemove = (id, e) => { 
    const word = e.target.previousSibling.innerText 
    console.log(word) 
    console.log(id) 
    alert(`You want to delete word - ${word}?`) 
    const wordsFilterd = words.filter( (w, index) => index !== id ) // 제거하려는 단어의 id 와 일치하는 요소만 걸러냄 
    setWords(wordsFilterd)
  } 
  
  const wordStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center' } 
    
  return ( 
    <div> 
      <h1 style={{textAlign:'center'}}>Word List</h1> 
      {words.map( (w, id) => { 
          return ( 
            <div key={id} style={wordStyle}> 
              <h2>{w.word}</h2> 
              <Button size="small" type="button" handleClick={(e) => handleRemove(id, e)}>DELETE</Button> 
            </div> 
          ) 
      })} 
    </div> 
 ); 
} 

export default App;

 

* 연습과제 7

import './App.css';
import React, { useState } from 'react';
import images from './imageData'
import Button from './Button'

function App() {
  const [index, setIndex] = useState(0)

  const decreaseIndex = () => {
    const nextIndex = index - 1
    setIndex((nextIndex < 0) ? images.length - 1 : nextIndex)
  }
  const increaseIndex = () => {
    const nextIndex = index + 1
    setIndex((nextIndex > images.length - 1) ? 0 : nextIndex)
  }

  const path = images[index].src
  const title = images[index].title
  return (
    <div className="App">
      <div className="img-container">
        <img src={path} alt={title}/>
      </div>

      <div className="control-btns">
        <Button handleClick={decreaseIndex}>Prev</Button>
        <Button handleClick={increaseIndex}>Next</Button>
      </div>
    </div>
  );
}

export default App;

 

* 연습과제 8

import './App.css';
import React, { useState } from 'react';
import Button from './Button'

function App() {
  const [id, setId] = useState('')
  const [password, setPassword] =  useState('')

  const handleChange = (e) => {
    const { name, value } = e.target
    console.log(name, value)

    name === "id"? setId(value) : setPassword(value) // 주석처리하면 사용자 입력이 되지 않음
  }
  const login = (e) => {
    e.preventDefault() // 새로고침 방지
    console.log('login')
  }
 
  return (
    <div className="App">
      <form>
          <label>ID <input type="text" placeholder="TYPE YOUR ID ..." name="id" value={id} onChange={handleChange}></input></label><br/><br/>
          <label>PASSWORD <input type="password" placeholder="TYPE YOUR PASSWORD ..." name="password" value={password} onChange={handleChange}></input></label>
          <div className="login-btn"><Button handleClick={login}>Login</Button></div>
      </form>
    </div>
  );
  
}

export default App;

 

* 연습과제 9

import './App.css';
import React, { useState, useRef } from 'react';
import Button from './Button'

function App() {
  const [fileName, setFileName] = useState('')
  const [imgSrc, setImgSrc] = useState('')

  const fileInput = useRef(null) // ref 생성하기

  const isValid = (type) => {
    return type === 'image'
  }
 
  const handleChange = (e) => {
    console.log(e.target.files[0])
    const file = e.target.files[0]
    const imgSrc = URL.createObjectURL(file)

    if(isValid(file.type.split('/')[0])){
      setFileName(file.name)
      setImgSrc(imgSrc)
    }else{
      setFileName('File is not valid type !')
      setImgSrc('')
    }
  }
  const openFileWindow = () => {
    fileInput.current.click() // ref 사용하기
  }
 
  return (
    <div className="App">
      <h1>{fileName}</h1>
      {imgSrc !== '' && <img src={imgSrc} alt="preview-img" width="300px" height="400px"></img> }
      <input className="Upload" type="file" onChange={handleChange} ref={fileInput} accept="image/*"></input>
      <Button handleClick={openFileWindow}>Upload</Button>  
    </div>
  );
  
}

export default App;
728x90