//Main.js
//스크롤 위치 구하는 함수
onScroll = (e) => {
const scrollTop = ("scroll", e.srcElement.scrollingElement.scrollTop);
this.setState({ scrollTop });
};
//스크롤 이벤트는 직접 window에 접근해서 선언하는 것이기 때문에
//componentWillUnmount() 를 통해 해당 컴포넌트가 언마운트 될 때 없애주어야한다.
//불필요한 메모리 손실을 막고 계속해서 이벤트가 서버에서 돌아가고 있는 상황을 방지할 수 있다.
componentWillUnmount() {
window.removeEventListener("scroll", this.onScroll);
}
componentDidMount() {
window.addEventListener("scroll", this.onScroll);
return(
...
// 스크롤 위치에 따라 classname을 부여해주면 그에따라 css 스타일링을 적용할 수 있다.
<ul className={scrollTop > 1300 ? "pop" : "none"}>
...
//TOP버튼이나 nav바에도 같은 방식으로 적용하였다.
)
/* Main.scss */
.none {
opacity: 0;
}
.pop {
transition: all 0.7s ease-in;
transform: translateY(-100px);
}
constructor() {
super();
this.state = {
data : [],
activeTab: "best",
tabClass: "activebest",
};
//onClick에서 받아온 prop(best나 new를 받아옴)을 가지고 state와 className를 바꾸기 위한 함수
handleClicked = (prop) => {
this.setState({ activeTab: prop, tabClass: `active${prop}` });
};
const newSort = [
...data.sort((a, b) => b.launchdate.localeCompare(a.launchdate)),
];
// 신상품 정렬을 위해 문자열과 문자열을 비교하는 localeCompare메소드를 활용하였다.
//api에서 불러온 data배열안에서 "2020-02-20"형식의 lauchdate의 값을 비교해서 큰값이 먼저 오도록해서 최신 날짜순이 되도록 했다.
//spread연산자를 사용해서 state를 직접 수정하지않고 새로운 배열로 만든다.
const newproduct = [];
newproduct.push(newSort[0], newSort[1], newSort[2]);
//최신 순으로 나열된 제품 중 상위3개만 추출해서 넣은 새로운 배열
const tab = {
//ProductList는 PRODUCT페이지에서도 제품목록을 리턴하기위해 사용되는 컴포넌트이다.
best: <ProductList products={bestproduct} />,
new: <ProductList products={newproduct} />,
};
return(
...
<div className="tabs">
<ul className="tabsTitle">
<li
className={tabClass}
onClick={() => this.handleClicked("best")}
>
<span>BEST</span>
</li>
<li
className={tabClass}
onClick={() => this.handleClicked("new")}
>
<span>NEW</span>
</li>
</ul>
//active의 초기값은 best이므로 베스트제품을 보여주고,
//위의 버튼 클릭에 따라 state가 바뀌면서 그에따라 다른 값이 전달 된ProductList 컴포넌트를 보여준다
<ul>{tab[activeTab]}</ul>
...
)
//PRODUCT페이지에서도 sort라는 state를 만들어서 같은 방식으로 적용하였다.
let list;
if (this.state.sort === "new") {
list = <ProductList products={newSorted} />;
} else if (this.state.sort === "best") {
list = <ProductList products={bestSorted} />;
} else {
list = <ProductList products={notSorted} />;
}
백엔드 API로 불러온 제품데이터 json형식은 다음과 같다.
{
"data": [
{
"id": 17,
"name": "워터리 쉬어 립스틱",
"price_krw": "18000.00",
"description": "촉촉하게 빛나는 #물빛입술",
"category": ["LIP"],
"color": [
{
"name": "이고르",
"stock_quantity": 1000,
"image_url": "https://laka.co.kr/web/product/option_button/20200323/d91df56843b5f052c0733a607c8538d3.jpg"
},
{
"name": "카미유",
"stock_quantity": 1000,
"image_url": "https://laka.co.kr/web/product/option_button/20200323/396244b15425120006155033c2fdb28e.jpg"
},
...
//ProductList.js
render() {
const map = this.props.products.map((item) => {
return (
<Link to="/ProductDetail" className="link" style={{ color: "black" }}>
<Item
key={item.id}
name={item.name}
price_krw={item.price_krw}
description={item.description}
color={item.color}
outer_front_image_url={item.outer_front_image_url}
outer_back_image_url={item.outer_back_image_url}
category={item.category}
/>
</Link>
);
});
return <ul>{map}</ul>;
}
//item.js
const {
name,
description,
color,
outer_front_image_url,
outer_back_image_url,
price_krw,
} = this.props;
//color배열안의 객체의 image_url값을 이미지로 전환
const colors = [];
for (let i in color) {
colors.push(color[i].image_url);
} //빈 배열을 만들고 각각의 url 주소를 넣는다
const url = colors.map((color, idx) => <img src={`${color}`} alt="" />);
// url로 이루어진 배열을 map함수를 이용해 img태그로 변환시켰다
//15000.00 형식으로오는 price_krw값을 15,000 형식으로 변환
//Number()로 문자열을 숫자로 바꾸고, 사용자의 문화권에 에 맞는 표기법으로 시간, 금액을 리턴하는 toLocaleString메서드를 이용하였다
const price = Number(price_krw).toLocaleString();
return (
<li className="item">
<div className="front">
<img src={`${outer_front_image_url}`} alt="" />
<div className="back"> // hover시 나오는 부분
<img
className="product-img"
src={`${outer_back_image_url}`}
alt=""
/>
<div className="item-info">
<div className="item-desc">
<p className="title">{name}</p>
<p> {description}</p>
<p className="color">{url}</p>
</div>
<div className="price">
<p>KRW {price}</p>
</div>
</div>
</div>
</div>
</li>
);
//Category.js
//ALL을 호버하면 나타나는 카테고리bar의 타이틀과 상품수도 백엔드데이터에서 받아오도록 하였다.
//각각의 타이틀을 클릭시 /product?category=타이틀 의 주소로 라우팅된다.
render() {
const { category } = this.state;
let address = this.props.location.search;
let categoryTitle = address.split("=")[1];
//각각의 카테고리에 대한 제품수를 받았지만 전체수량이 없었기때문에 합계를 구하는 함수를 생성
const allCount = () => {
let count = 0;
for (let i in category) {
count += category[i].count;
}
return count;
};
return (
//카테고리수가 열가지정도 되는 데 각각 div를 만들어줬는데, 불러온 배열 이용해서 map으로 나열해도 됐을것같다.
<div className="nav_container">
<div className="nav">
<div className="category">
<div className="categoryAll">
//처음에 ALL이었던 타이틀 누르면 이동하는 카테고리에 맞게 타이틀이 바뀌어야함.
//location객체에 담긴 주소값을 활용해서 타이틀을 변환시켰다.
{categoryTitle === undefined ? (
<span>ALL</span>
) : (
<span>{categoryTitle}</span>
)}
<div className="categoryImg">
<img
className="icon"
src="https://laka.co.kr/assets/ko/images/ico/ico_arr.png"
alt=""
/>
</div>
</div>
<div className="list">
<div className="row">
<span onClick={() => this.props.history.push("/product")}>
ALL
<span className="count">{allCount()}</span>
</span>
<span
onClick={() =>
this.props.history.push("/product?category=FACE")
}
>
FACE
<span className="count">
{category[0] && category[0].count}
//cdm에서 fetch된 후에야 리턴할 값이 존재하는것이다. category에 데이터가 들어와서 true일때 값을 반환가능!
</span>
class Store extends Component {
constructor() {
super();
this.state = {
stores: [],
userInput: "",
};
}
//API에서 불러온 지점목록을 stores라는 프로퍼티의 값으로 저장
componentDidMount() {
fetch(`${API}/store`)
.then((res) => res.json())
.then((res) => this.setState({ stores: res.data }));
}
//자식컴포넌트에서 유저가 입력한 값을 받아와서 state에 업데이트한다.
onSearchSubmit = (value) => {
this.setState({ userInput: value });
};
render() {
//state에 담긴 stores에 filter 메소드를 사용하여 스토어목록이 유저 입력값이 주소에 포함된 것만 필터링 되도록했다.
//빈문자열 상태인 초기에는 전부 보여지게된다.
const filtered = this.state.stores.filter((store) =>
store.address.includes(this.state.userInput)
);
return (
<div className="wrapper">
<Nav />
<div className="store-page">
<img
src="https://laka.co.kr/laka_skin/images/pc/store_visual.jpg"
alt=""
/>
<div className="storeBox">
//
<Search onSubmit={this.onSearchSubmit} />
//Search컴포넌트에서 받아온 input값에 따라 필터링된 배열을 StoreList의 props로 전달
<StoreList stores={filtered} />
//StoreList컴포넌트는 자식컴포넌트인 Store컴포넌트(각각의 스토어들)를 map을 이용해 목록으로return하는 컴포넌트이다.
</div>
</div>
<Footer />
</div>
);
}
}
//Search.js
class Search extends Component {
constructor(props) {
super(props);
this.state = {
userInput: "",
selected: "",
};
}
//저장된 state값을 부모 컴포넌트로 전달하기 위한 함수
onFormSubmit = (e) => {
//form태그에 걸려있는 이벤트기때문에 페이지가 리로딩되는 것을 막아주어야한다.
e.preventDefault();
//부모컴포넌트에서 전달받은 props인 onSubmit을 통해 자식컴포넌트의 state값을 전달한다
this.props.onSubmit(this.state.userInput);
};
onFormSelect = (e) => {
this.props.onSubmit(this.state.selected);
};
//검색입력창에 쓴 값을 저장
handleUserInput = (e) => {
this.setState({ userInput: e.target.value });
};
//드롭다운에서 선택한 값을 저장
handleSelect = (e) => {
this.setState({ selected: e.target.value }, () => this.onFormSelect(e));
//드롭다운에서 선택시 따로 엔터나 검색버튼클릭을 하지 않으므로 바로 부모컴포넌트로 전달함.
//setState가 비동기함수이기때문에 두번째 인자로 부모컴포넌트로 입력값을 전달할 수 있는 콜백함수를 전달하였다.
};
render() {
const { userInput, selected } = this.state;
return (
<div className="Search">
<form onSubmit={this.onFormSubmit}>
<select
className="select"
onChange={this.handleSelect}
value={selected}
className="myClassName"
>
<option value="">전체</option>
<option value="서울">서울</option>
<option value="경기">경기</option>
<option value="인천">인천</option>
<option value="부산">부산</option>
<option value="대구">대구</option>
<option value="광주">광주</option>
<option value="대전">대전</option>
<option value="울산">울산</option>
<option value="충청">충청</option>
<option value="전라">전라</option>
</select>
<div className="textInput">
<input
type="text"
value={userInput}
onChange={this.handleUserInput}
placeholder="매장명으로 검색하세요."
/>
<button type="submit" className="searchButton">
검색
</button>
</div>
</form>
</div>
);
}
}
썸네일 컴포넌트 (Item.js) 에서 커서를 올리면 다른이미지와 텍스트가 나오는데 처음에 onMouseover 이벤트를 이용해서 state를 변화시켜, state에 따라 다른 div가 보이도록 했다.
그런데 뭔가 부드럽게 넘어가는 것 같지않고 버벅이는 느낌이었다.
다른방식을 생각해보니 단순히 CSS hover를 이용하여 호버될때와 안될때의 div 두개를 처음부터 렌더링하고, opacity나 z-index를 조절하는 것이다.
처음 state를 활용한 방식은 가상DOM렌더부터 시작하여 레이아웃, 페인트, Composite되는 과정을 거쳐야하지만,
바꾼 방식은 브라우저종류에 따라 다르지만 크롬에서는 페인팅부터만 다시 적용되기때문에 더 부드러워지는 것 같다. https://csstriggers.com/
그리고 굳이 필요없는 state관리를 한다는 것 자체가 오버인듯함!?