Redux 연동하기
버전이 다르기 때문에 소스가 강좌와 다를 수 있다.
버전
next: 13.0.4
antd: 5.0.1
소스 : https://github.dev/braverokmc79/node-bird-sns
13. 리덕스 설치와 필요성 소개
설치
$ npm i next-redux-wrapper
$ npm i react-redux redux redux-promise redux-thunk
중앙데이타저장소 Store
- 컴포넌트에서 공통적으로 쓰이는 데이타가 흩어져있기 때문에 부모 컴포넌트에서 데이타를 받아서 자식 컴포넌트에게 각각 보내줘야한다
- 컴포넌트끼리 데이타를 전달하는 과정도 매우 복잡하고 오류가 발생하기도 쉽다.

- 규모가 있는 서비스라면 중앙데이타저장소 Store를 최소 한 개 만들어 중앙에서 모든 데이타를 관리하고 보내주는 것이 편하다
- 중앙데이타저장소는 Redux, React의 contextedAPI, Mobax, Apolo 등이 있다
Redux
- Redux는 원리가 간단하고 모든 수정사항을 기록한다.
- 모든 히스토리가 기록되어 에러 추적이 쉽고 안정적이다.
- 하지만 코드량이 많아진다.
- 중앙은 앱이 커지면 데이타를 쪼개는 작업이 필요한데 Redux는 reducer들을 쪼개는 기능을 제공해주어 편리하다 (reducer란 전달받은 action에 따라 상태를 어떻게 업데이트할지 정의해주는 함수)
- 사용법이 쉬워 초보자에게 추천되지만 전체적으로 가장 많이 사용되는 라이브러리기도 하다
Mobax
- Mobax는 코드량이 매우 적어 생산성을 높일 수 있다
- 하지만 실수를 했을 때 추적이 어렵다
- 초보를 벗어나면 생산성을 위해 추천되는 라이브러리지만 Redux보다는 사용률이 낮다
Context API
- 앱 규모가 작을 때 권장된다
- 큰 규모의 프로젝트는 차라리 리덕스나 모백스를 사용하는 것이 낫다
- 중앙데이타저장소는 서버에서 데이타를 받아오는데 이 과정은 비동기다
- 서버가 고장나거나 네트워크 에러가 생기면 데이타가 안 올수 있다
- 실패에 대비하기위해 요청, 성공, 실패 3단계를 직접 구현해야한다.
- 3단계 구현 코드를 컴포넌트마다 넣어줘야해 의도치 않은 코드 중복이 발생
- 위의 코드만 밖으로 따로 빼낼 수 있지만 비동기 요청이 많으면 context API도 리덕스와 모백스와 비슷해진다
- 따라서 리덕스나 모백스 등이 알아서 처리해주는 것이 편하다
useEffect(() => {
axios.get('/data')
.then(() => {
setState(data);
})
.catch(() => {
setError(error);
})
})
Redux && redux-wrapper
설치
넥스트가 설치되어있는 front 디렉토리에서 리덕스 설치
npm i reduxRedux의 손쉬운 사용을 위해 redux-wrapper 설치 (Redux와 사용법이 약간 다르다)
npm i next-redux-wrapperfront/store/configureStore.js 파일 생성
import { createWrapper } from 'next-redux-wrapper';
import {createStore} from 'redux';
const configureSotre = () => {
const store = createStore(reducer);
return store;
};
const wrapper = createWrapper(configureSotre, {
debug: process.env.NODE_ENV === 'development,'
});
export default wrapper;
- _app.js 파일 수정
- wrapper 불러오기
- wrapper를 사용해서 내보내기
- Nodebird는 앱 이름
import wrapper from '../store/configureStore.js`
.
.
.
export default wrapper.withRedux(Nodebird);
//wrapper로 감싸줘야 프로젝트의 모든 컴포넌트와 페이지에 적용된다
- 예전에는 _app.js의 return 내부를 Provider로 감싸야했는데 지금은 redux가 알아서 감싸주니 비워두어야한다 (사용자가 감싸면 중복되어 에러 발생)
//before
return (
<Provider store={store}>
</Provider>
);
//from redux@6
return (
<>
</>
);
14. 리덕스의 원리와 불변성
Redux의 원리
진행 과정

- 앱의 상태를 객체 형식으로 작성한다 state (좌)
- 변경하고 싶은 내용을 action으로 만든다 (우)
- action의 이름을 적는 type
- 변경사항을 적는 data
- action을 dispatch한다
- dispatch된 action을 reducer에 따라 처리한다 (아래)
- reducer는 action 어떻게 처리할지 정의해준다
- 예를 들어 switch를 값을 변경해주는 것이다
- case에 action의 type을 적어준다
- 바꾸고싶은 값에 action.data를 넣어준다
action을 생성해서 무엇으로 변경할지 적어준다
- 변경사항마다 action을 만들어줘야한다
{
type: 'CHANGE_NICKNAME' // action이름
data: 'boogicho' // 변경될 데이타값
}
{
type: 'CHANGE_AGE'
data: 30,
}
15. 리덕스의 원리와 불변성
pages/_app.js
~ import wrapper from '../store/configureStore'; ~ export default wrapper.withRedux(NodeBird);
$ npm i redux-devtools-extension
store/configureStore.js
import { createWrapper } from 'next-redux-wrapper';
import { compose, applyMiddleware, legacy_createStore as createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import reducer from '../reducers';
const configureSotre = () => {
const middlewares = [];
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
return store;
};
const wrapper = createWrapper(configureSotre, {
debug: process.env.NODE_ENV === 'development,'
});
export default wrapper;
// import Reducer from './_reducers';
// import { applyMiddleware, legacy_createStore as createStore } from 'redux';
// import promiseMiddleware from 'redux-promise';
// import ReduxThunk from 'redux-thunk';
// const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
// const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// createStoreWithMiddleware(Reducer, devTools)
reducer/index.js
const initialState = {
user: {
isLoggedIn: false,
user: null,
signUpdata: {},
loginData: {}
},
post: {
mainPosts: []
}
}
export const loginAction = (data) => {
return {
type: "LOG_IN",
data
}
}
export const logoutAction = () => {
return {
type: "LOG_OUT"
}
}
//(이전상태,액션) => 다음 상태
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOG_IN':
return {
...state,
user: {
...state.user,
isLoggedIn: true,
user: action.data
}
}
case 'LOG_OUT':
return {
...state,
user: {
...state.user,
isLoggedIn: false,
user: null
}
}
default:
return state;
}
}
export default rootReducer;
components/AppLayout.js
import { useSelector } from 'react-redux';
~
const AppLayout = ({ children }) => {
const isLoggedIn = useSelector((state) => state.user.isLoggedIn);
~
~
<Col xs={24} md={6} className="mt-20">
{isLoggedIn ? <UserProfile /> : <LoginForm />}
</Col>
~
componets/UserProfile.js
import React, { useCallback } from 'react';
import { Card, Avatar, Button } from 'antd';
import { useDispatch } from 'react-redux';
import { logoutAction } from '../reducers';
const UserProfile = () => {
const dispatch = useDispatch();
const onLogOut = useCallback(() => {
dispatch(logoutAction(false));
}, []);
~
16. 미들웨어와 리덕스 데브툴즈
강의 :
$ npm i redux-devtools-extension
import { createWrapper } from 'next-redux-wrapper';
import { compose, applyMiddleware, legacy_createStore as createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import reducer from '../reducers';
const configureSotre = () => {
const middlewares = [];
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
return store;
};
const wrapper = createWrapper(configureSotre, {
debug: process.env.NODE_ENV === 'development,'
});
export default wrapper;
// import Reducer from './_reducers';
// import { applyMiddleware, legacy_createStore as createStore } from 'redux';
// import promiseMiddleware from 'redux-promise';
// import ReduxThunk from 'redux-thunk';
// const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
// const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// createStoreWithMiddleware(Reducer, devTools)
17. 리듀서 쪼개기
reducers/index.js
import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';
import user from './user';
import post from './post';
//(이전상태,액션) => 다음 상태
const rootReducer = combineReducers({
index: (state = {}, action) => {
switch (action.type) {
case HYDRATE:
console.log(' HYDRATE ', action);
return { ...state, ...action.payload };
default:
return state;
}
},
user,
post
});
export default rootReducer;
reducers/user.js
export const initialState = {
isLoggedIn: false,
user: null,
signUpdata: {},
loginData: {}
}
export const loginAction = (data) => {
return {
type: "LOG_IN",
data
}
}
export const logoutAction = () => {
return {
type: "LOG_OUT"
}
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'LOG_IN':
return {
...state,
isLoggedIn: true,
user: action.data
}
case 'LOG_OUT':
return {
...state,
isLoggedIn: false,
user: null
}
default:
return state;
}
}
export default reducer;
reducers/post.js
const initialState = {
mainPosts: [],
}
const reducer = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
}
export default reducer;
[Next.js] Hydrate란?
Hydrate란, Server Side단에서 렌더링 된 정적 페이지와 번들링된 js파일(Webpack)을 클라이언트에게 보낸 뒤, 클라이언트 단에서 HTML 코드와 React인 js 코드를 서로 매칭시키는 과정을 말합니다.
일단, Client Side Rendering인 기본 React에서는 js파일만을 이용하여 public/index.html의 기본 뼈대만 있는 내용을 제외하고 src/index.js의 자바스크립트 코드에서 모든 화면을 렌더링 한 뒤, HTML DOM 요소 중 root라는 엘리먼트를 찾아 하위로 주입을 하여 웹 화면이 구성합니다.
Next.js에서는 클라이언트에게 웹 페이지를 보내기 전에 Server Side 단에서 미리 웹 페이지를 Pre-Rendering 합니다.
Pre-Rendering하면 HTML document가 생성되고, 이 파일을 클라이언트에게 전송합니다.
이때, 클라이언트가 받은 웹페이지는 단순한 웹 화면만 보여주는 HTML일 뿐이고 자바스크립트 요소는 하나도 없는 상태입니다. 이는 웹 화면을 보여주고 있지만, 특정 JS 모듈 뿐만 아니라 단순 클릭과 같은 이벤트 리스너들도 각 웹페이지의 DOM 요소에 적용되어 있지 않은 상태를 말합니다.
- React.js : Html과 JS파일을 한꺼번에 보내고 클라이언트가 js 코드를 통해 웹 화면을 렌더링
- Next.js : Pre-Rendering된 웹 페이지를 클라이언트에게먼저 보내고, React가 번들링된 자바스크립트 코드들을 클라이언트에게 전송함.
참조 : https://narup.tistory.com/230
18. 더미데이터와 포스트폼 만들기
강의 :
pages/index.js
import React from 'react';
import AppLayout from './../components/AppLayout';
import { useSelector } from 'react-redux';
import PostCard from './../components/PostCard';
import PostForm from './../components/PostForm';
const Index = () => {
const { isLoggedIn } = useSelector((state) => state.user);
const { mainPosts } = useSelector((state) => state.post);
return (
<AppLayout>
{isLoggedIn && <PostForm />}
{mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
</AppLayout>
);
};
export default Index;
components/PostCard.js
import React from 'react';
const PostCard = () => {
return (
<div>
PostCard
</div>
);
};
export default PostCard;
components/PostForm.js
import React, { useCallback, useRef, useState } from 'react';
import { Form, Input, Button } from 'antd';
import { useSelector, useDispatch } from 'react-redux';
import { addPost } from '../reducers/post';
const PostForm = () => {
const { imagePaths } = useSelector((state) => state.post);
const dispatch = useDispatch();
const imageInput = useRef();
const [text, setText] = useState('');
const onChangeText = useCallback((e) => {
setText(e.target.value);
}, []);
const onSubmit = useCallback(() => {
dispatch(addPost);
setText("");
}, []);
const onClickImageUpload = useCallback(() => {
imageInput.current.click();
}, [imageInput.current]);
return (
<Form style={{ margin: '10px 0 20px' }} encType="multipart/form-data" onFinish={onSubmit}>
<Input.TextArea
value={text}
onChange={onChangeText}
maxLength={140}
placeholder="어떤 신기한 일이 있었나요?"
/>
<div>
<input type="file" multiple hidden ref={imageInput} style={{ display: "none" }} />
<Button onClick={onClickImageUpload}>이미지 업로드</Button>
<Button type="primary" htmlType='submit' style={{ float: 'right' }} >짹짹</Button>
</div>
<div>
{
imagePaths.map((v) => (
<div key={v} style={{ display: "inline-block" }}>
<img src={v} style={{ width: '200px' }} alt={v} />
<div>
<Button>제거</Button>
</div>
</div>
))
}
</div>
</Form>
);
};
export default PostForm;
19. 게시글 구현하기
components/PostCard.js
import React, { useState, useCallback } from 'react';
import { Card, ButtonGroup, Button, Avatar, Image, Popover, Space } from 'antd';
import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone } from '@ant-design/icons';
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux';
import PostImages from './PostImages';
const PostCard = ({ post }) => {
const id = useSelector((state) => state.user.me?.id);
const [liked, setLiked] = useState(false);
const [commentFormOpened, setCommentFormOpened] = useState(false);
const onToggleLike = useCallback(() => {
setLiked((prev) => !prev);
}, []);
const onToggleComment = useCallback(() => {
setCommentFormOpened((prev) => !prev);
}, [])
const content = (
<div>
{id && post.User.id === id ? (
<>
<Button>수정</Button>
<Button type='danger'>삭제</Button>
</>
) : (
<Button>신고</Button>
)
}
</div >
);
return (
<div style={{ marginBottom: 30 }}>
<Card
cover={post.Images[0] && <PostImages images={post.Images} />}
actions={[
<RetweetOutlined key="retweet" />,
liked ? <HeartTwoTone key="heart" twoToneColor="#ebef96" onClick={onToggleLike} /> :
<HeartOutlined key="heart" onClick={onToggleLike} />,
<MessageOutlined key="comment" onClick={onToggleComment} />,
<Popover content={content} title="더보기" key="popover" style={{ textAlign: "center" }}>
<EllipsisOutlined />
</Popover>
]}
>
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={post.content}
/>
<Image />
<Button></Button>
</Card >
{commentFormOpened && (
<div>
댓글 부분
</div>
)}
{/* <CommentForm />
<Comments /> */}
</div >
);
};
PostCard.propTypes = {
post: PropTypes.shape({
id: PropTypes.number,
User: PropTypes.object,
content: PropTypes.string,
createdAt: PropTypes.object,
Comment: PropTypes.arrayOf(PropTypes.object),
Images: PropTypes.arrayOf(PropTypes.object)
}).isRequired
}
export default PostCard;
components/PostImages.js
import React from 'react';
const PostImages = () => {
return (
<div>
PostImages
</div>
);
};
export default PostImages;
20. 댓글 구현하기
components/PostCard.js
import React, { useState, useCallback } from 'react';
import { Card, Button, Avatar, Image, Popover, List } from 'antd';
import { Comment } from '@ant-design/compatible';
import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone } from '@ant-design/icons';
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux';
import PostImages from './PostImages';
import CommentForm from './CommentForm';
//import Comment from './Comment';
const PostCard = ({ post }) => {
const id = useSelector((state) => state.user.me?.id);
const [liked, setLiked] = useState(false);
const [commentFormOpened, setCommentFormOpened] = useState(false);
const onToggleLike = useCallback(() => {
setLiked((prev) => !prev);
}, []);
const onToggleComment = useCallback(() => {
setCommentFormOpened((prev) => !prev);
}, [])
const content = (
<div>
{id && post.User.id === id ? (
<>
<Button>수정</Button>
<Button type='danger'>삭제</Button>
</>
) : (
<Button>신고</Button>
)
}
</div >
);
return (
<div style={{ marginBottom: 30 }}>
<Card
cover={post.Images[0] && <PostImages images={post.Images} />}
actions={[
<RetweetOutlined key="retweet" />,
liked ? <HeartTwoTone key="heart" twoToneColor="#ebef96" onClick={onToggleLike} /> :
<HeartOutlined key="heart" onClick={onToggleLike} />,
<MessageOutlined key="comment" onClick={onToggleComment} />,
<Popover content={content} title="더보기" key="popover" style={{ textAlign: "center" }}>
<EllipsisOutlined />
</Popover>
]}
>
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={post.content}
/>
<Image />
</Card >
{commentFormOpened && (
<div>
<CommentForm post={post} />
<List
header={`${post.Comments.length} 개의 댓글`}
itemLayout="horizontal"
dataSource={post.Comments}
renderItem={(item) => (
<li>
<Comment
author={item.User.nickname}
avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
content={item.content}
/>
</li>
)}
/>
</div>
)}
{/* <CommentForm />
<Comments /> */}
</div >
);
};
PostCard.propTypes = {
post: PropTypes.shape({
id: PropTypes.number,
User: PropTypes.object,
content: PropTypes.string,
createdAt: PropTypes.object,
Comment: PropTypes.arrayOf(PropTypes.object),
Images: PropTypes.arrayOf(PropTypes.object)
}).isRequired
}
export default PostCard;
21. 이미지 구현하기
components/PostImages.js
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { PlusOutlined } from '@ant-design/icons';
const PostImages = ({ images }) => {
const [showImageZoom, setShowImagesZoom] = useState(false);
const onZoom = useCallback(() => {
setShowImagesZoom(true);
}, []);
if (images.length === 1) {
return (
<>
<img role="presentation" src={images[0].src} alt={images[0].src}
onClick={onZoom} width="50%" />
</>
)
} else if (images.length === 2) {
return (
<>
<div>
<img role="presentation" src={images[0].src} alt={images[0].src} onClick={onZoom} style={{ width: "50%" }} />
<img role="presentation" src={images[1].src} alt={images[1].src} onClick={onZoom} style={{ width: "50%" }} />
</div>
</>
)
} else if (images.length > 2) {
return (
<>
<div>
<img role="presentation" src={images[0].src} alt={images[0].src}
onClick={onZoom} style={{ width: "50%" }} />
<div
role="presentation"
style={{ display: 'inline-block', width: '50%', textAlign: 'center', verticalAlign: 'middle' }}
onClick={onZoom}
>
<PlusOutlined />
<br />
{images.length - 1}
개의 사진 더보기
</div>
</div>
</>
);
}
};
PostImages.propTypes = {
image: PropTypes.object.isRequired
}
export default PostImages;
22. 이미지 캐루셀 구현하기(react-slick)
강의 :
슬릭 슬라이드 :
$ npm i react-slick $ npm install slick-carousel
components/ImagesZoom/index.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Slick from 'react-slick';
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import styled, { createGlobalStyle } from 'styled-components';
const Overlay = styled.div`
position:fixed;
z-index:5000;
top:0;
left:0;
right:0;
bottom:0;
`;
const Header = styled.header`
height:44px;
background:white;
position:relative;
padding:0;
text-align:center;
& h1{
margin:0;
font-size:17px;
color:#333;
line-height:44px;
}
& button{
position:absolute;
right:0;
top:0;
padding:15px;
line-height:10px;
cursor:pointer;
background: #faad14;
color: #fff;
border: 1px solid #fa8c16;
}
`;
const SlickWrapper = styled.div`
height:calc(100% -44px);
background:rgba(0, 0, 0, 0.9);
border:none !important;
height: 100%;
`;
const ImageWrapper = styled.div`
padding:32px;
text-align:center;
cursor:pointer;
&img{
margin:0 auto;
max-height:750px;
display: inline-block !important;
}
`;
const Indicator = styled.div`
text-align :cetner;
& > div{
width:75px;
height:30px;
line-height:30px;
border-radius:15px;
background:#313131;
display:inline-block;
text-align:center;
top:60px;
background:#fff;
position: relative;
}
`;
const Global = createGlobalStyle`
.slick-slide{
display:inline-block;
}
.ant-card-cover{
transform:none !important;
}
.slick-dots li button:before{
color:#fff;
opacity:1;
}
.slick-dots li.slick-active button:before {
opacity: 1;
color:#faad14;
}
.slick-prev, .slick-nex{
width: 50px;
height: 50px;
}
.slick-prev {
left: 15%;
}
.slick-next{
right:15%;
}
`;
const ImagesZoom = ({ images, onClose }) => {
const settings = {
dots: true,
infinite: true,
speed: 1000,
//autoplay: true,
autoplayspeed: 1000,
slidesToShow: 1,
slidesToScroll: 1,
arrows: false,
pauseOnHover: true,
};
const [currentSlide, setCurrentSlide] = useState(0);
return (
<Overlay>
<Global />
<Header>
<h1>상세 이미지 {images.length}</h1>
<button onClick={onClose} >x</button>
</Header>
<SlickWrapper>
<div>
<Slick
// settings={settings}
initialSlide={0}
afterChange={(slide) => setCurrentSlide(slide)}
infinite={true}
dots={true}
arrows={true}
slidesToShow={1}
slidesToScroll={1}
>
{images && images.map((v) => {
return (
<ImageWrapper key={v.src}>
<img src={v.src} alt={v.src} />
</ImageWrapper>
)
})}
</Slick>
<Indicator>
<div>
{currentSlide + 1}
{' / '}
{images.length}
</div>
</Indicator>
</div>
</SlickWrapper>
</Overlay>
);
};
ImagesZoom.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
onClose: PropTypes.func.isRequired,
}
export default ImagesZoom;

23. 글로벌 스타일과 컴포넌트 폴더 구조
강의 :
components/ImagesZoom/styles.js
import styled, { createGlobalStyle } from 'styled-components';
export const Overlay = styled.div`
position:fixed;
z-index:5000;
top:0;
left:0;
right:0;
bottom:0;
`;
export const Header = styled.header`
height:44px;
background:white;
position:relative;
padding:0;
text-align:center;
& h1{
margin:0;
font-size:17px;
color:#333;
line-height:44px;
}
& button{
position:absolute;
right:0;
top:0;
padding:15px;
line-height:10px;
cursor:pointer;
background: #faad14;
color: #fff;
border: 1px solid #fa8c16;
}
`;
export const SlickWrapper = styled.div`
height:calc(100% -44px);
background:rgba(0, 0, 0, 0.9);
border:none !important;
height: 100%;
`;
export const ImageWrapper = styled.div`
padding:32px;
text-align:center;
cursor:pointer;
&img{
margin:0 auto;
max-height:750px;
display: inline-block !important;
}
`;
export const Indicator = styled.div`
text-align :cetner;
& > div{
width:75px;
height:30px;
line-height:30px;
border-radius:15px;
background:#313131;
display:inline-block;
text-align:center;
top:60px;
background:#fff;
position: relative;
}
`;
export const Global = createGlobalStyle`
.slick-slide{
display:inline-block;
}
.ant-card-cover{
transform:none !important;
}
.slick-dots li button:before{
color:#fff;
opacity:1;
}
.slick-dots li.slick-active button:before {
opacity: 1;
color:#faad14;
}
.slick-prev, .slick-nex{
width: 50px;
height: 50px;
}
.slick-prev {
left: 15%;
}
.slick-next{
right:15%;
}
`;
components/ImagesZoom/index.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Slick from 'react-slick';
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { Overlay, Global, Header, SlickWrapper, ImageWrapper, Indicator } from "./styles";
const ImagesZoom = ({ images, onClose }) => {
const settings = {
dots: true,
infinite: true,
speed: 1000,
//autoplay: true,
autoplayspeed: 1000,
slidesToShow: 1,
slidesToScroll: 1,
arrows: false,
pauseOnHover: true,
};
const [currentSlide, setCurrentSlide] = useState(0);
return (
<Overlay>
<Global />
<Header>
<h1>상세 이미지 {images.length}</h1>
<button onClick={onClose} >x</button>
</Header>
<SlickWrapper>
<div>
<Slick
// settings={settings}
initialSlide={0}
//afterChange={(slide) => setCurrentSlide(slide)}
beforeChange={(slide) => setCurrentSlide(slide)}
infinite={true}
dots={true}
arrows={true}
slidesToShow={1}
slidesToScroll={1}
>
{images && images.map((v) => {
return (
<ImageWrapper key={v.src}>
<img src={v.src} alt={v.src} />
</ImageWrapper>
)
})}
</Slick>
<Indicator>
<div>
{currentSlide + 1}
{' / '}
{images.length}
</div>
</Indicator>
</div>
</SlickWrapper>
</Overlay>
);
};
ImagesZoom.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
onClose: PropTypes.func.isRequired,
}
export default ImagesZoom;
24. 게시글 해시태그 링크로 만들기
강의
:
정규식 사이트
components/PostCard.js
import PostCardContent from './PostCardContent';
~
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={<PostCardContent postData={post.content} />}
/>
~
components/PostCardContent.js
import React from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link';
const PostCardContent = ({ postData }) => {
//첫번째 게시글 #해시태그 #익스프레스
return (
<div>
{postData.split(/(#[^\s#]+)/g).map((v, index) => {
if (v.match(/(#[^\s#]+)/)) {
return <Link key={index} href={`/hashtag/${v.slice(1)}`}>{v}</Link>
}
return v;
})}
</div>
);
};
PostCardContent.propTypes = {
postData: PropTypes.string.isRequired
}
export default PostCardContent;














댓글 ( 4)
댓글 남기기