일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 엄티로드
- 프로그래밍
- JavaScript
- development
- 주짓떼라
- git
- nodejs
- Redux
- graphQL
- REACT
- 영화감상
- 드릴
- 클로즈가드
- 개발
- 파이썬
- 리액트
- 개발자
- 자바스크립트
- 노드
- web
- 하프가드
- 디자인패턴
- 주짓떼로
- 영화
- 영화리뷰
- 솔로드릴
- 주짓수
- Node
- Express
- 웹개발
- Today
- Total
As i wish
[React SNS] React SNS 만들기 - 2 본문
안녕하세요. 엄티입니다.
계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.
프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.
참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.
이번에는 화면을 만들었는데요. 메인 화면, 프로필 화면을 만들고 그에 필요한 컴포넌트들을 분리하였습니다.
먼저 pages/_app.js 를 사용하였는데 이는 next에서 제공해주는 것으로
<html>
<head>
</head>
<body>
root
</body>
</html>
_document.js -> html, head, body 담당
_app.js -> root 담당
pages -> pages
_error.js -> error 발생 시
이런식의 순서라고 보면 될것 같네요.
pages/_app.js
import React from 'react';
import Head from 'next/head';
import PropTypes from 'prop-types';
import AppLayout from '../components/AppLayout';
const NodeBird = ({ Component }) => {
return (
<>
<Head>
<title>NodeBird</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.20.5/antd.css"/>
</Head>
<AppLayout>
<Component/>
</AppLayout>
</>
);
};
NodeBird.propTypes = {
Component: PropTypes.elementType
}
export default NodeBird;
_app.js 는 props 로 component 를 받는데 이 component 들이 pages가 됩니다.
그럼 pages 들은 공통된 부분인 <Head></Head>, <AppLayout></AppLayout> 을 계속 갖고 있게 되는거죠.
지난번 코드에서는 공통된 부분을 각 페이지 별로 계속해서 써주었었는데 참으로 간단해 졌죠?
그 다음 eslint 설정에 의하여 타입을 지정해 주지 않아서 몇몇 에러가 나는것들이 있습니다.
이는 prop-types 를 사용해서 타입을 지정해 주면 좋습니다.
사실 저는 js 강점이 타입을 따로 지정해 주지 않아도 되는 점이라고 생각했는데 요 며칠 계속 코딩을 하다보니 타입을 지정해 주지 않게 되면
많은 버그들이 나오고 협업하는데 문제가 생길 수도 있을것 같더라고요.
그래서 타입을 지정해주는 버릇을 다시 들이도록 노력해야겠습니다.
타입들은 다 외울 필요는 없고 필요한것들을 공식문서를 통해서 확인해서 사용하면 될것 같습니다.
위의 pages/_app.js 의 props 인 Component 는 elementType이라고 할 수 있겠네요. 만약 타입을 지정해주었는데 그에 맞는 타입이 아닌경우 에러가 납니다. 어렵지 않아요!!
그 다음 우리는 antd 를 사용하고 있는데 이에 맞게 화면을 분할 해주는 Grid 시스템을 사용하고 있습니다.
저는 화면 구성을 할 때에 정말 제 머리속에서 생각해서 모바일, 작은화면, 중간화면, 큰화면 등으로 세분하게 나누어서 화면을 구성하곤 했었는데 이런 시스템이 있는지는 몰랐습니다. 앞으로 꼭 antd 를 사용하지 않더라도 참고하여서 사용해야겠습니다.
실제 제가 일을 할 때에는 bootstrap을 사용했었는데 어떨 때에는 이러한 것들이 실제 회사에서 하고자 하는 방향과 다를 때가 있더라고요.
그때에는 bootstrap을 걷어내기도 했었는데 어떻게 보면 양날의 검 같습니다. 실제 구현은 빠르고 편안하게 해주지만 정말 커스텀 하게 사용하기에는 조금 불편한 경우도 있더라고요. 상황에 맞게 잘 사용하는것이 좋습니다.
실제 디자이너가 있을 때에는 정말 로우하게 도움을 받고 css로 구성하는것이 좋을것 같네요.
components/AppLayout.js
import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
import { Menu, Input, Row, Col } from 'antd';
import LoginForm from '../components/LoginForm';
import UserProfile from '../components/UserProfile';
const mock = {
nickname: 'MockNickName',
Post: [],
Followings: [],
Followers: [],
isLoggedIn: false
};
const AppLayout = ({ children }) => {
return (
<div>
<Menu mode="horizontal">
<Menu.Item key="home"><Link href="/"><a>NodeBird</a></Link></Menu.Item>
<Menu.Item key="profile"><Link href="/profile"><a>Profile</a></Link></Menu.Item>
<Menu.Item key="mail"><Input.Search enterButton style={{ verticalAlign: 'middle' }}/></Menu.Item>
</Menu>
<Row gutter={8}>
<Col xs={24} md={6}>
{
mock.isLoggedIn
? <UserProfile/>
: <LoginForm/>
}
</Col>
<Col xs={24} md={12}>{children}</Col>
<Col xs={24} md={6}><Link href="https://www.zerocho.com"><a target="_blank">Made by ZeroCho</a></Link></Col>
</Row>
</div>
)
};
AppLayout.propTypes = {
children: PropTypes.node
}
export default AppLayout;
grid 시스템을 사용해서 AppLayout 을 구성해 봤는데요. props 로 받는 children은 각각의 페이지가 됩니다. (index.js, profile.js, signup.js) Row, Col 을 이용한것이 grid 시스템인데 총 화면의 가로 길이를 24 로 했을 때에 Col 의 xs={24} 의 의미는 24/24 를 사용하겠다는 의미입니다. 즉 모바일일 떄에(xs 는 화면크기가 모바일을 의미) 화면을 가득 채우겠다는 의미이지요.
그에 반하여 md={6}, md={12}, md={6} 은 화면의 크기가 middle 일 때에 화면을 각각 6:12:6 으로 사용하겠다는 의미입니다.
Row 에 gutter 은 확인해 보니까 각 Col 의 padding-left + padding-right 값이더군요.
components/AppLayout.js 에서 isLoggedIn이 true이면 UserProfile을 보여주고 아니면 LoginForm을 보여주게 되어있는데, 이는 로그인 한 유저는 로그인한 유저 정보, 하지 않은 유저는 로그인 폼을 보여주는 것입니다.
components/LoginForm.js
import React, { useState, useCallback } from 'react';
import Link from 'next/link';
import { Form, Input, Button } from 'antd'
// Custom hook
export const useInput = (initValue = null) => {
const [value, setter] = useState(initValue);
const handler = useCallback((e) => {
setter(e.target.value);
}, []);
return [value, handler];
}
const LoginForm = () => {
const [id, onChangeId] = useInput('');
const [pass, onChangePass] = useInput('');
const onSubmitForm = useCallback((e) => {
e.preventDefault();
console.log(id, pass);
}, [id, pass]);
return (
<>
<Form onSubmit={onSubmitForm} style={{ padding: '10px' }}>
<div>
<label htmlFor="user-id">ID</label>
<br/>
<Input name="user-id" required value={id} onChange={onChangeId}/>
</div>
<div>
<label htmlFor="user-pass">Password</label>
<br/>
<Input name="user-pass" type="password" required value={pass} onChange={onChangePass}/>
</div>
<div style={{marginTop: '10px' }}>
<Button type="primary" htmlType="submit" loading={false}>LogIn</Button>
<Link href="/signup"><a><Button>SignUp</Button></a></Link>
</div>
</Form>
</>
)
}
export default LoginForm;
components/LoginForm 에서는 지난 포스팅에서 만들었던 customHook 을 사용하였고 export 시켰는데, 이는 pages/signup.js 에서 사용됩니다.
pages/signup.js
import React, { useState, useCallback } from 'react';
import { Form, Input, Checkbox, Button } from 'antd';
import { useInput } from '../components/LoginForm';
const Signup = () => {
// const [id, setId] = useState(''); // Using custom hook in bottom
// const [nick, setNick] = useState(''); // Using custom hook in bottom
const [pass, setPass] = useState('');
const [passChk, setPassChk] = useState('');
const [term, setTerm] = useState('');
const [passError, setPassError] = useState(false);
const [termError, setTermError] = useState(false);
const onSubmit = useCallback((e) => {
e.preventDefault();
if (pass !== passChk) {
return setPassError(true);
}
if (!term) {
return setTermError(true);
}
console.log({
id, nick, pass, passChk, term
})
}, [pass, passChk, term]);
// const onChangeId = (e) => { // Using custom hook in bottom
// setId(e.target.value);
// };
// const onChangedNick = (e) => {
// setNick(e.target.value);
// };
const onChangePass = useCallback((e) => {
setPass(e.target.value);
}, []);
const onChangePassChk = useCallback((e) => {
setPassError(e.target.value !== pass);
setPassChk(e.target.value);
}, [pass]);
const onChangeTerm = (e) => {
setTermError(false);
setTerm(e.target.checked);
};
const [id, onChangeId] = useInput('');
const [nick, onChangedNick] = useInput('');
return (
<>
<Form onSubmit={onSubmit} style={{ padding: 10 }}>
<div>
<label htmlFor="user-id">ID</label>
<br/>
<Input name="user-id" required value={id} onChange={onChangeId}/>
</div>
<div>
<label htmlFor="user-nick">Nickname</label>
<br/>
<Input name="user-nick" required value={nick} onChange={onChangedNick}/>
</div>
<div>
<label htmlFor="user-pass">Password</label>
<br/>
<Input name="user-pass" type="password" required value={pass} onChange={onChangePass}/>
</div>
<div>
<label htmlFor="user-pass-chk">Password Check</label>
<br/>
<Input name="user-pass-chk" type="password" required value={passChk} onChange={onChangePassChk}/>
{passError && <div style={{ color: 'red' }}>Please check password</div>}
</div>
<div>
<Checkbox name="user-term" value={term} onChange={onChangeTerm}>
Are you agree this term?
</Checkbox>
{termError && <div style={{ color: 'red' }}>You must agrre term</div>}
</div>
<div>
<Button type="primary" htmlType="submit">Register</Button>
</div>
</Form>
</>
);
};
export default Signup;
components/UserProfile.js
import React from 'react';
import { Card, Avatar } from 'antd'
const mock = {
nickname: 'MockNickName',
Post: [],
Followings: [],
Followers: [],
isLoggedIn: false
};
const UserProfile = () => {
return (
<>
<Card
actions={[
<div key="twit">짹짹<br />{mock.Post.length}</div>,
<div key="following">팔로잉<br />{mock.Followings.length}</div>,
<div key="follower">팔로워<br />{mock.Followers.length}</div>,
]}
>
<Card.Meta
avatar={<Avatar>{mock.nickname[0]}</Avatar>}
title={mock.nickname}
/>
</Card>
</>
)
}
export default UserProfile;
Card 는 antd 를 확인해 주세요.
여기까지가 AppLayout.js 를 구성한 부분이고 다음은 main page(pages/index.js) 입니다.
pages/index.js
import React from 'react'; // eslint 에서 react 쓰면 import 하라고 명시됨
import PostForm from '../components/PostForm';
import PostCard from '../components/PostCard';
const mock = {
isLoggedIn: true,
imagePaths: [],
mainPosts: [{
User: {
id: 1,
nickname: 'MockNickName',
},
content: 'First card',
img: 'https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726',
}],
};
const Home = () => {
return (
<>
<div>
{mock.isLoggedIn && <PostForm imagePaths={mock.imagePath}/>}
{
mock.mainPosts.map((v) => {
return <PostCard key={v} post={v} />
})
}
</div>
</>
);
};
export default Home;
main 화면인데 만약 isLoggedIn 이 true 이면 PostForm 을 보여줍니다. 로그인과 상관 없이는 PostCard 를 보여주는데 PostForm은 PostCard 를 쓰는 창이라고 생각하면 됩니다.
components/PostForm.js
import React from 'react';
import PropTypes from 'prop-types';
import { Form, Input, Button } from 'antd'
const PostForm = ({ imagePaths }) => {
return (
<>
<Form style={{ margin: '10px 0 20px' }} encType="multipart/form-data">
<Input.TextArea maxLength={140} placeholder='How was your day?'/>
<div>
<input type="file" multiple hidden/>
<Button>Upload Image</Button>
<Button type="primary" htmlType="submit">TWIT</Button>
</div>
<div>
{
!!imagePaths
&& imagePaths.map((v) => {
return (
<div key={v}>
<img src={`http://localhost:3000/${v}`} alt={v}/>
<div>
<Button>Remove</Button>
</div>
</div>
)
})
}
</div>
</Form>
</>
)
};
PostForm.propTypes = {
imagePaths: PropTypes.array
}
export default PostForm;
components/PostCard.js
import React from 'react';
import PropTypes from 'prop-types';
import { Card, Avatar, Icon, Button } from 'antd'
const PostCard = ({ post }) => {
return (
<>
<Card
cover={post.img && <img alt="example" src={post.img}/>}
actions={[
<Icon type="retweet" key="retweet" />,
<Icon type="heart" key="heart" />,
<Icon type="message" key="message" />,
<Icon type="ellipsis" key="ellipsis" />,
]}
extra={<Button>팔로우</Button>}
>
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={post.content}
/>
</Card>
</>
)
}
PostCard.propTypes = {
post: PropTypes.shape({
User: PropTypes.object,
content: PropTypes.string,
img: PropTypes.string,
createdAt: PropTypes.object,
})
}
export default PostCard;
Card는 components/UserProfile.js 위에서 한번 설명하였고요 PropTypes.shape 를 사용하면 객체 안에 요소들에 대하여 타입을 지정해 줄 수 있습니다.
여기까지가 메인페이지 (pages/index.js) 부분이고 다음은 프로필(pages/profile.js) 부분입니다.
마지막으로 프로필 화면을 디자인하고 포스팅을 마치겠습니다.
pages/profile.js
import React from 'react';
import { List, Card, Icon, Button } from 'antd';
import NicknameEditForm from '../components/NicknameEditForm';
const Profile = () => {
return (
<>
<NicknameEditForm/>
<List
style={{ marginTop: '20px' }}
grid={{ gutter: 4, xs: 2, md: 3 }}
size="small"
header={<div>Following</div>}
dataSource={['Following_1', 'Following_2', 'Following_3']}
loadMore={<Button style={{ width: '100%' }}>LoadMore</Button>}
bordered
renderItem={item => (
<List.Item style={{ marginTop: '20px' }}>
<Card actions={[<Icon key="stop" type="stop" />]}><Card.Meta description={item} /></Card>
</List.Item>
)}
/>
<List
style={{ marginTop: '20px' }}
grid={{ gutter: 4, xs: 2, md: 3 }}
header={<div>Follower</div>}
dataSource={['Follower_1', 'Follower_2', 'Follower_3']}
loadMore={<Button style={{ width: '100%' }}>LoadMore</Button>}
bordered
renderItem={item => (
<List.Item style={{ marginTop: '20px' }}>
<Card actions={[<Icon key="stop" type="stop" />]}><Card.Meta description={item} /></Card>
</List.Item>
)}
/>
</>
);
};
export default Profile;
profile 화면은 다음과 같이 닉네임을 고칠 수 있는 곳과 팔로잉, 팔로워 목록을 보여주는 곳으로 나뉘죠.
팔로잉, 팔로워 부분은 antd 를 사용하였고 이는 antd List 를 참고 하시면 될것 같습니다.
components/NicknameEditForm.js
import React from 'react';
import { Form, Input, Button } from 'antd'
const NicknameEditForm = () => {
return (
<Form style={{ marginTop: '20px'}}>
<Input style={{ marginBottom: '10px' }} addonBefore="닉네임"/>
<Button type="primary">Change</Button>
</Form>
)
};
export default NicknameEditForm;
닉네임을 수정하는 컴포넌트는 다음과 같이 구성합니다. 어렵지 않죠?
마지막으로 어떤 코드마다 const mock={} 이렇게 정의된 부분이 있는데, 이는 서버에서 받아올 데이터를 가짜 데이터 처럼 만들어 준것입니다. 실제 프론트엔드 개발을 할 때에도 서버가 완성 되어있으면 좋겠지만 그럴일은 거의 없기 때문에 가짜 데이터를 생성해 놓고 개발을 하는 경우가 많은데 저 또한 많이 사용하는 방법입니다.
profilePage 적용 (pages/profile.js)
이렇게 오늘은 메인페이지, 포르필 페이지를 다 디자인 했는데 제가 혹시 빼먹은 antd 는 직접 antd사이트에 가셔서 참고 부탁드립니다.
그외의 빼먹은 코드는 저의 깃허브 커밋메세지를 통해 보실 수 있습니다. [2-*] 이렇게 시작하는 것들이 오늘 포스팅에 수정되고 추가된 코드들입니다.
실행 화면
'React JS' 카테고리의 다른 글
[React SNS] React SNS 만들기 - 4 - 1 (redux saga 란, airbnbStyle applied) (0) | 2019.08.12 |
---|---|
[React SNS] React SNS 만들기 - 3 (0) | 2019.08.06 |
[React SNS] React SNS 만들기 - 1 (0) | 2019.07.29 |
[React JS] BookMark - 1 (0) | 2019.07.18 |
[React 틱 택 토] Redux - React 프로젝트 사용 (2) | 2019.07.18 |