이번시간에는 최근검색어 기능을 구현해보겠습니다.
localStorage - https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
filter - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
json.stringfy - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
json.parse - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
useEffect - https://ko.reactjs.org/docs/hooks-reference.html#useeffect
useState - https://ko.reactjs.org/docs/hooks-reference.html#usestate
styled-components - https://styled-components.com/docs/basics
저는 네이버 모바일버전 검색 화면 처럼 레이아웃을 짜봤습니다.
import React, { useState, useEffect } from 'react'
import History from '../components/search/history.js'
import SearchBar from '../components/search/search-bar.js'
function SearchPage() {
return (
<div>
<SearchBar/>
<History/>
</div>
)
}
export default SearchPage
import React, { useState } from 'react'
import styled, { css } from 'styled-components'
import { Link } from 'react-router-dom'
const horizontalCenter = css`
position: absolute;
top: 50%;
transform: translateY(-50%);
`
const Container = styled.div`
position: relative;
width: 100%;
border-bottom: 2px solid #0bde8b;
background-color: #fff;
padding: 20px 60px;
box-sizing: border-box;
`
//라우터의 Link 스타일을 입히는거임(페이지이동하는 버튼)
//horizontalCenter 스타일 컴포넌트를 믹스인하여 속성값 전달
//홈으로 가기 위한 뒤로가기 버튼입니다
const ArrowIcon = styled(Link)`
${horizontalCenter}
left: 18px;
display: block;
width: 21px;
height: 18px;
background-position: -164px -343px;
vertical-align: top;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
const SearchIcon = styled.span`
${horizontalCenter}
right: 18px;
width: 24px;
height: 24px;
background-position: -356px -260px;
display: inline-block;
overflow: hidden;
color: transparent;
vertical-align: middle;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
//글자를 입력하면 RemoveIcon이 나오게 되고 누르면 input의 value값이 사라집니다
const RemoveIcon = styled.span`
${horizontalCenter}
right: 0px;
width: 20px;
height: 20px;
background-position: -389px -29px;
display: inline-block;
overflow: hidden;
color: transparent;
vertical-align: top;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
const InputContainer = styled.div`
position: relative;
`
const Input = styled.input`
width: 100%;
background-color: #fff;
font-weight: 700;
font-size: 20px;
box-sizing: border-box;
`
function SearchBar() {
return (
<Container>
<ArrowIcon to="/" />
<InputContainer>
<Input
placeholder="검색어를 입력해주세요"
/>
<RemoveIcon/>
</InputContainer>
<SearchIcon />
</Container>
)
}
export default SearchBar
import React from 'react'
import styled from 'styled-components'
const HistoryContainer = styled.div`
padding: 18px;
`
const HeaderContainer = styled.div`
overflow: hidden;
`
const Title = styled.span`
float: left;
font-weight: 400;
color: #666;
`
const RemoveText = styled.span`
float: right;
color: #a7a7a7;
`
const ListContainer = styled.ul`
margin: 10px 0;
`
//&는 자기 자신을 나타냄
//즉, 나 자신(li)들에서 마지막 요소 값을 제외한 값에 margin-bottom 속성 지정
const KeywordContainer = styled.li`
overflow: hidden;
&:not(:last-child) {
margin-bottom: 10px;
}
`
const RemoveButton = styled.button`
float: right;
color: #0cde8b;
border: 1px solid #0cde8b;
padding: 3px 5px;
border-radius: 15px;
`
const Keyword = styled.span`
font-size: 18px;
font-weight: 400;
`
function History() {
return (
<HistoryContainer>
<HeaderContainer>
<Title>최근 검색어</Title>
<RemoveText>전체삭제</RemoveText>
</HeaderContainer>
<ListContainer>
<KeywordContainer>
<Keyword></Keyword>
<RemoveButton>
삭제
</RemoveButton>
</KeywordContainer>
</ListContainer>
</HistoryContainer>
)
}
export default History
검색 버튼을 눌렀을때 검색한 키워드를 keyword 상태값에 담기 위한handleAddKeyword
삭제 버튼을 눌렀을때 눌린 해당 키워드만을 지우기 위한 handleRemoveKeyword
전체 삭제를 눌렀을때 모든 keyword 상태값을 지우기 위한 handleClearKeywords
++ search-bar 컴포넌트에서 검색버튼을 눌렀을때 search 페이지의 keyword 상태값에 계속 추가가 되고(localStorage로 추가가 되야합니다. 그래야 새로고침이나 창을 꺼도 그 기록값이 사라지지 않습니다.)
/ 추가된 상태값들을 map으로 리스트를 뽑아 히스토리에 뿌립니다.
++ 뽑힌 리스트의 삭제를 누르면 그 요소 버튼의 unique한 id값과 keyword 배열안에 들어있는 id값을 필터하여 삭제시킬 값을 제거합니다.
++ 전체 삭제를 누르면 모든 keyword값을 제거합니다.
이로써 위 기능들을 모두 구현한 코드를 종합적으로 보면
import React, { useState, useEffect } from 'react'
import History from '../components/search/history.js'
import SearchBar from '../components/search/search-bar.js'
function SearchPage() {
//string은 map을 사용 할 수 없기때문에 object 형태로 변환 시키기 위해 parsing을 해줘야함
const [keywords, setKeywords] = useState(
JSON.parse(localStorage.getItem('keywords') || '[]'),
)
//keyword에 변화가 일어날때만 랜더링
useEffect(() => {
//array 타입을 string형태로 바꾸기 위해 json.stringfy를 사용한다.
localStorage.setItem('keywords', JSON.stringify(keywords))
}, [keywords])
//state를 다루는 함수는 handle 보통 많이 붙인다.
//검색어 추가
const handleAddKeyword = (text) => {
console.log('text', text)
const newKeyword = {
id: Date.now(),
text: text,
}
setKeywords([newKeyword, ...keywords])
}
//검색어 삭제
const handleRemoveKeyword = (id) => {
const nextKeyword = keywords.filter((thisKeyword) => {
return thisKeyword.id != id
})
setKeywords(nextKeyword)
}
//검색어 전체 삭제
const handleClearKeywords = () => {
setKeywords([])
}
//자식 컴포넌트에서 setState를 못하기때문에 그거를 바꿔주는 함수를 선언후 그 함수를 넘겨야함
return (
<div>
<SearchBar onAddKeyword={handleAddKeyword}></SearchBar>
<History
keywords={keywords}
onClearKeywords={handleClearKeywords}
onRemoveKeyword={handleRemoveKeyword}
/>
</div>
)
}
export default SearchPage
import React, { useState } from 'react'
import styled, { css } from 'styled-components'
import { Link } from 'react-router-dom'
const horizontalCenter = css`
position: absolute;
top: 50%;
transform: translateY(-50%);
`
const Container = styled.div`
position: relative;
width: 100%;
border-bottom: 2px solid #0bde8b;
background-color: #fff;
padding: 20px 60px;
box-sizing: border-box;
`
//Link태그의 스타일을 입히는거임(페이지이동하는 버튼)
//horizontalCenter 스타일 컴포넌트를 믹스인하여 속성값 전달
//홈으로 가기 위한 뒤로가기 버튼입니다
const ArrowIcon = styled(Link)`
${horizontalCenter}
left: 18px;
display: block;
width: 21px;
height: 18px;
background-position: -164px -343px;
vertical-align: top;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
const SearchIcon = styled.span`
${horizontalCenter}
right: 18px;
width: 24px;
height: 24px;
background-position: -356px -260px;
display: inline-block;
overflow: hidden;
color: transparent;
vertical-align: middle;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
//글자를 입력하면 RemoveIcon이 나오게 되고 누르면 input의 value값이 사라집니다
const RemoveIcon = styled.span`
${horizontalCenter}
right: 0px;
width: 20px;
height: 20px;
background-position: -389px -29px;
display: inline-block;
overflow: hidden;
color: transparent;
vertical-align: top;
background-image: url(https://s.pstatic.net/static/www/m/uit/2020/sp_search.623c21.png);
background-size: 467px 442px;
background-repeat: no-repeat;
`
const InputContainer = styled.div`
position: relative;
`
const Input = styled.input`
width: 100%;
background-color: #fff;
font-weight: 700;
font-size: 20px;
box-sizing: border-box;
${({ active }) =>
active &&
`
padding-right: 25px;
`}
`
function SearchBar({ onAddKeyword }) {
// 1. 검색어를 state 로 다루도록 변경
// 2. 이벤트 연결
// 3. Link to 설명
//form을 관련 요소를 다룰때는 2-way 데이터 바인딩을 해줍니다! (input 의 value에 state를 넣는 것)
const [keyword, setKeyword] = useState('')
const handleKeyword = (e) => {
setKeyword(e.target.value)
}
const handleEnter = (e) => {
if (keyword && e.keyCode === 13) {
//엔터일때 부모의 addkeyword에 전달
onAddKeyword(keyword)
setKeyword('')
}
}
const handleClearKeyword = () => {
setKeyword('')
}
//느낌표로 키워드를 갖고있냐 없냐로 boolean 형태로 나옴
//키워드를 가지고 있다면 active가 발생하여 padding이 발생함. // 패딩이 없으면 x 아이콘까지 글자가 침법하기 때문
const hasKeyword = !!keyword
{
//keyword가 있으면 true, 없으면 false가 리턴이 되는 것을 확인 할 수 있습니다
console.log(!!keyword)
}
return (
<Container>
<ArrowIcon to="/" />
<InputContainer>
<Input
placeholder="검색어를 입력해주세요"
active={hasKeyword}
value={keyword}
onChange={handleKeyword}
onKeyDown={handleEnter}
/>
{keyword && <RemoveIcon onClick={handleClearKeyword} />}
</InputContainer>
<SearchIcon />
</Container>
)
}
export default SearchBar
import React from 'react'
import styled from 'styled-components'
const HistoryContainer = styled.div`
padding: 18px;
`
const HeaderContainer = styled.div`
overflow: hidden;
`
const Title = styled.span`
float: left;
font-weight: 400;
color: #666;
`
const RemoveText = styled.span`
float: right;
color: #a7a7a7;
`
const ListContainer = styled.ul`
margin: 10px 0;
`
//&는 자기 자신을 나타냄
//즉, 나 자신(li)들에서 마지막 요소 값을 제외한 값에 margin-bottom 속성 지정
const KeywordContainer = styled.li`
overflow: hidden;
&:not(:last-child) {
margin-bottom: 10px;
}
`
const RemoveButton = styled.button`
float: right;
color: #0cde8b;
border: 1px solid #0cde8b;
padding: 3px 5px;
border-radius: 15px;
`
const Keyword = styled.span`
font-size: 18px;
font-weight: 400;
`
function History({ keywords, onRemoveKeyword, onClearKeywords }) {
console.log('keyword', keywords)
if (keywords.length === 0) {
return <HistoryContainer>최근 검색된 기록이 없습니다.</HistoryContainer>
}
return (
<HistoryContainer>
<HeaderContainer>
<Title>최근 검색어</Title>
<RemoveText onClick={onClearKeywords}>전체삭제</RemoveText>
</HeaderContainer>
<ListContainer>
{keywords.map(({ id, text }) => {
return (
<KeywordContainer key={id}>
<Keyword>{text}</Keyword>
<RemoveButton
//눌렸을때 해야하는거라 arrow function을 사용하여 실행
//그냥 함수 쓰면은 그려지자마자 바로 실행됨
onClick={() => {
onRemoveKeyword(id)
}}
>
삭제
</RemoveButton>
</KeywordContainer>
)
})}
</ListContainer>
</HistoryContainer>
)
}
export default History
가 됩니다 !
늦었지만 엄청나게 도움을 얻고 갑니다 좋은 게시글 공유 감사합니다 ㅎㅎ