As i wish

[React SNS] React SNS 만들기 - 2 본문

React JS

[React SNS] React SNS 만들기 - 2

어면태 2019. 7. 30. 11:54

안녕하세요. 엄티입니다.

계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.

 

프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.

React SNS 만들기 - 1

 

[React SNS] React SNS 만들기 - 1

안녕하세요. 엄티입니다. 오늘은 React 를 사용하여 간단한 SNS 를 만들어 볼껀데요. 제 포스팅은 '제로초' 님의 강의를 바탕으로 복습용으로 작성됩니다. 자세한 설명을 원하면 밑에 강의를 참조해 주세요. '제로..

eomtttttt-develop.tistory.com

참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.

'제로초' 님의 React NodeBird SNS

 

React로 NodeBird SNS 만들기 - 인프런

리액트&넥스트&리덕스&리덕스사가&익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다. 중급 웹 개발 프레임워크 및 라이브러리 서비스 개발 Front End Back End Javascript React 온라인 강의

www.inflearn.com

 

이번에는 화면을 만들었는데요. 메인 화면, 프로필 화면을 만들고 그에 필요한 컴포넌트들을 분리하였습니다.

먼저 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> 을 계속 갖고 있게 되는거죠.

지난번 코드에서는 공통된 부분을 각 페이지 별로 계속해서 써주었었는데 참으로 간단해 졌죠?

 

_app.js 적용 commit

 

[2-1] pages/_app.js by duplicate component in pages · eomttt/react-sns@2c34799

Permalink Browse files [2-1] pages/_app.js by duplicate component in pages Loading branch information... Showing 4 changed files with 66 additions and 73 deletions. +19 −0 front/pages/_app.js +3 −12 front/pages/index.js +3 −12 front/pages/profile.js +41 −4

github.com

그 다음 eslint 설정에 의하여 타입을 지정해 주지 않아서 몇몇 에러가 나는것들이 있습니다. 

이는 prop-types 를 사용해서 타입을 지정해 주면 좋습니다. 

사실 저는 js 강점이 타입을 따로 지정해 주지 않아도 되는 점이라고 생각했는데 요 며칠 계속 코딩을 하다보니 타입을 지정해 주지 않게 되면

많은 버그들이 나오고 협업하는데 문제가 생길 수도 있을것 같더라고요.

그래서 타입을 지정해주는 버릇을 다시 들이도록 노력해야겠습니다.

 

prop-types npm

 

prop-types

Runtime type checking for React props and similar objects.

www.npmjs.com

타입들은 다 외울 필요는 없고 필요한것들을 공식문서를 통해서 확인해서 사용하면 될것 같습니다.

위의 pages/_app.js 의 props 인 Component 는 elementType이라고 할 수 있겠네요. 만약 타입을 지정해주었는데 그에 맞는 타입이 아닌경우 에러가 납니다. 어렵지 않아요!!

 

그 다음 우리는 antd 를 사용하고 있는데 이에 맞게 화면을 분할 해주는 Grid 시스템을 사용하고 있습니다.

 

antd grid system

 

Ant Design - A UI Design Language

Use row-flex define flex layout, its child elements depending on the value of the start,center, end,space-between, space-around, which are defined in its parent node layout mode.

ant.design

저는 화면 구성을 할 때에 정말 제 머리속에서 생각해서 모바일, 작은화면, 중간화면, 큰화면 등으로 세분하게 나누어서 화면을 구성하곤 했었는데 이런 시스템이 있는지는 몰랐습니다. 앞으로 꼭 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 를 확인해 주세요.

 

antd Card

 

Ant Design - A UI Design Language

A Card that supports cover, avatar, title and description.

ant.design

여기까지가 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 를 사용하면 객체 안에 요소들에 대하여 타입을 지정해 줄 수 있습니다.

 

mainPage 적용 (pages/index.js)

 

[2-1, 2, 3, 4, 5] PropTypes, Grid system, Reuse customHook, MainPage · eomttt/react-sns@93fdbca

Permalink Browse files [2-1, 2, 3, 4, 5] PropTypes, Grid system, Reuse customHook, MainPage Loading branch information... Showing 11 changed files with 235 additions and 48 deletions. +0 −21 front/.eslintrc +21 −0 front/.eslintrc.json +31 −18 front/compone

github.com

여기까지가 메인페이지 (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 를 참고 하시면 될것 같습니다.

antd list

 

Ant Design - A UI Design Language

Ant Design, a design language for background applications, is refined by Ant UED Team.

ant.design

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)

 

[2-6, 7] Add profile pages · eomttt/react-sns@f87bf10

Permalink Browse files [2-6, 7] Add profile pages Loading branch information... Showing 2 changed files with 44 additions and 3 deletions. +13 −0 front/components/NicknameEditForm.js +31 −3 front/pages/profile.js @@ -0,0 +1,13 @@ import React from 'react';

github.com

 

이렇게 오늘은 메인페이지, 포르필 페이지를 다 디자인 했는데 제가 혹시 빼먹은 antd 는 직접 antd사이트에 가셔서 참고 부탁드립니다.

그외의 빼먹은 코드는 저의 깃허브 커밋메세지를 통해 보실 수 있습니다. [2-*] 이렇게 시작하는 것들이 오늘 포스팅에 수정되고 추가된 코드들입니다.

 

gitHub code

 

eomttt/react-sns

React sns test by react-nodebird(zeroCho). Contribute to eomttt/react-sns development by creating an account on GitHub.

github.com

ant.design

 

Ant Design - A UI Design Language

 

ant.design

실행 화면

components/AppLayout isLogged = true, pages/index.js isLogged = true 일 때 화면
components/AppLayout isLogged = false, pages/index.js isLogged = true 일 때 화면 로그인 화면 대신 내 프로필이 보이죠?
components/AppLayout isLogged = true, pages/index.js isLogged = false 일 때 화면 포스팅을 쓰는 컴포넌트가 없죠?
pages/profile.js

 

 

오늘의 폴더 구조

Comments