[사이드 프로젝트] snsbird #3 LoginForm, 리렌더링 이해하기, 더미데이터로 로그인하기

devMag 개발 블로그·2022년 5월 2일

1. 더미 데이터 이용하기

  • 우선 로그인을 위해서는 서버가 필요하다. 그렇지만 현재 서버 셋팅이 안되있기 때문에 더미 데이터를 써야한다.
  • 먼저 state를 통해서 로그인 되있다는 상태를 더미로 설정하자.
  • 로그인이 되있다면 UserProfile 컴포넌트를, 아니라면 LoginForm 컴포넌트를 보여주게 된다.
    • 일단 작성해두고 나중에 컴포넌트를 만들면 된다.
// AppLayout.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link';
import { Menu, Input, Row, Col } from 'antd';
import 'antd/dist/antd.css';

import UserProfile from '../components/UserProfile';
import LoginForm from '../components/LoginForm';

const AppLayout = ({ children }) => {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    return (
        <div>
            <Menu mod="horizontal">
                <Menu.Item>
                    <Link href="/"><a>노드버드</a></Link>
                </Menu.Item>
                <Menu.Item>
                    <Link href="/profile"><a>프로필</a></Link>
                </Menu.Item>
                <Menu.Item>
                    <Input.Search enterButton style={{ verticalAlign: 'middle' }} />
                </Menu.Item>
                <Menu.Item>
                    <Link href="signup"><a>회원가입</a></Link>
                </Menu.Item>
            </Menu>
            <Row gutter={8}>
                <Col xs={24} md={6} >
                    {isLoggedIn ? <UserProfile /> : <LoginForm />}
                </Col>
                <Col xs={24} md={12}>
                    {children}
                </Col>
                <Col xs={24} md={6}>
                    <a href="https://www.zerocho.com" target="_blank" rel="noreferrer noopener">Made by Zerocho</a>
                </Col>
            </Row>
        </div>
    );
};

AppLayout.propTypes = {
    children: PropTypes.node.isRequired,
}

export default AppLayout;

2. LoginForm 만들기

  • LoginForm을 components 폴더에 만들어주자.
  • 컴포넌트에 props로 넘겨주는 함수는 useCallback을 써줘야 최적화가 된다.
// LoginForm.js

import React, { useState, useCallback } from 'react';
import { Form, Input, Button } from 'antd';
import Link from 'next/link';

const LoginForm = () => {
    const [id, setId] = useState('');
    const [password, setPassword] = useState('');

    const onChangeId = useCallback((e) => {
        setId(e.target.value);
    }, []);

    const onChangePassword = useCallback((e) => {
        setPassword(e.target.value);
    }, []);

    return (
        <Form>
            <div>
                <label htmlFor="user-id">아이디</label>
                <br />
                <Input
                    name="user-id"
                    value={id}
                    onChange={onChangeId}
                    required
                />
            </div>
            <div>
                <label htmlFor="user-password">비밀번호</label>
                <br />
                <Input
                    name="user-password"
                    value={password}
                    onChange={onChangePassword}
                    required
                />
            </div>
            <div style={{ marginTop: 10 }}>
                <Button type="primary" htmlType="submit" loading={false}>로그인</Button>
                <Link href="/signup"><a><Button>회원가입</Button></a></Link>
            </div>
        </Form>
    );
}

export default LoginForm;

3. 리렌더링 이해하기

  • 보통 귀찮아서 style={{}} 라는 식으로 객체를 넣어준다. 실제로 사용할 수 있는 inline style이다. 그렇지만 이렇게 사용하는건 지양해야한다.
  • 왜냐하면 렌더링이 될 때 inline에 적혀 있는 객체는 렌더링 후 객체와 같지 않아서({} === {}, false) 다시 리렌더링이 일어나기 때문이다.

참고
코딩하는 갓디노 | react에서 inline style 적용 방법

  • 그래서 실제로 변한 게 없어도, 가상돔이 어디가 변했나 찾다가 객체가 달라졌다고 인식해버린다.
  • 아래와 같이 작성하면 자식 태그들이 리렌더링이 되버린다.
<div style={{ marginTop: 10 }}>
	<Button type="primary" htmlType="submit" loading={false}>로그인</Button>
	<Link href="/signup"><a><Button>회원가입</Button></a></Link>
</div>

해결 방안 1 : styled-components

  • 이럴 때 styled-components 를 사용하면 된다.
// LoginForm.js
import React, { useState, useCallback } from 'react';
import { Form, Input, Button } from 'antd';
import Link from 'next/link';
import styled from 'styled-components';

const ButtonWrapper = styled.div`
    margin-top: 10px;
`;

const LoginForm = () => {
    const [id, setId] = useState('');
    const [password, setPassword] = useState('');

    const onChangeId = useCallback((e) => {
        setId(e.target.value);
    }, []); // 컴포넌트에 props로 넘겨주는 함수는 useCallback을 써줘야 최적화가 된다.

    const onChangePassword = useCallback((e) => {
        setPassword(e.target.value);
    }, []);

    return (
        <Form>
            <div>
                <label htmlFor="user-id">아이디</label>
                <br />
                <Input
                    name="user-id"
                    value={id}
                    onChange={onChangeId}
                    required
                />
            </div>
            <div>
                <label htmlFor="user-password">비밀번호</label>
                <br />
                <Input
                    name="user-password"
                    value={password}
                    onChange={onChangePassword}
                    required
                />
            </div>
            <ButtonWrapper>
                <Button type="primary" htmlType="submit" loading={false}>로그인</Button>
                <Link href="/signup"><a><Button>회원가입</Button></a></Link>
            </ButtonWrapper>
        </Form>
    );
}

export default LoginForm;

물론 성능에 영향이 없다면 크게 집착할 필요는 없다. 하지만 앱이 커질수록 렌더링을 최적화하는 게 필수적이므로 평소에 습관을 들여놓자.


  • AppLayout.js 에도 인라인 스타일로 작성된 부분이 있다. 같이 고쳐주자.
  • styled-components는 컴포넌트도 커스텀하게 스타일링이 가능하다.
// AppLayout.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link';
import { Menu, Input, Row, Col } from 'antd';
import 'antd/dist/antd.css';
import styled from 'styled-components';

import UserProfile from '../components/UserProfile';
import LoginForm from '../components/LoginForm';

const SearchInput = styled(Input.Search)`
    vertical-align: middle;
`;

const AppLayout = ({ children }) => {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    return (
        <div>
            <Menu mod="horizontal">
                <Menu.Item>
                    <Link href="/"><a>노드버드</a></Link>
                </Menu.Item>
                <Menu.Item>
                    <Link href="/profile"><a>프로필</a></Link>
                </Menu.Item>
                <Menu.Item>
                    <SearchInput enterButton />
                </Menu.Item>
                <Menu.Item>
                    <Link href="signup"><a>회원가입</a></Link>
                </Menu.Item>
            </Menu>
            <Row gutter={8}>
                <Col xs={24} md={6} >
                    {isLoggedIn ? <UserProfile /> : <LoginForm />}
                </Col>
                <Col xs={24} md={12}>
                    {children}
                </Col>
                <Col xs={24} md={6}>
                    <a href="https://www.zerocho.com" target="_blank" rel="noreferrer noopener">Made by Zerocho</a>
                </Col>
            </Row>
        </div>
    );
};

AppLayout.propTypes = {
    children: PropTypes.node.isRequired,
}

export default AppLayout;

해결 방안 2. useMemo 사용하기

  • styled-components이외에도 useMemo를 사용하는 방법이 있다.

useMemo : 값을 캐싱
useCallback : 함수를 캐싱

  • useMemo를 이용해서 값을 캐싱하면 객체가 이전 값이랑 같기 때문에 리렌더링이 되지 않는다.
// LoginForm.js

...
const style = useMemo(() => ({ marginTop: 10 }), []);
...

<ButtonWrapper style={style}>
	<Button type="primary" htmlType="submit" loading={false}>로그인</Button>
	<Link href="/signup"><a><Button>회원가입</Button></a></Link>
</ButtonWrapper>

리렌더링 될 때 모든 부분이 다시 그려지나?
그건 아니다. 함수형 컴포넌트는 일단 내부 코드가 전부 재시작되는 것은 맞다. (useCallback이나 useMemo는 일단 이전과 같은거라고 취급한다.) 그리고 return 부분에서 바뀐 부분만 다시 그리게 된다.
쉽게 설명해서 return 부분이 virtual dom이다. 일단 맨 처음에는 한 번 그려주고 그 이후 리렌더링이 뙬 때 이전 virtual dom과 현재 virtual dom을 비교하고 바뀐 부분만 다시 그리게 되는 것이다.


4. 더미 데이터로 로그인하기

  • Form에 onFinish={함수} 를 넣으면 e.preventDefault() 같은걸 자동으로 처리해준다.
  • UserProfile.js 와 LoginForm.js에 setIsLoggedIn 을 props로 내려주자.
// AppLayout.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Menu, Input, Row, Col } from 'antd';
import 'antd/dist/antd.css';
import styled from 'styled-components';

import UserProfile from './UserProfile';
import LoginForm from './LoginForm';

const SearchInput = styled(Input.Search)`
    vertical-align: middle;
`;

const AppLayout = ({ children }) => {
    const router = useRouter();
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    return (
        <div>
            <Menu mode="horizontal" selectedKeys={[router.pathname]}>
                <Menu.Item key="/">
                    <Link href="/"><a>노드버드</a></Link>
                </Menu.Item>
                <Menu.Item key="/profile">
                    <Link href="/profile"><a>프로필</a></Link>
                </Menu.Item>
                <Menu.Item key="/search">
                    <SearchInput enterButton />
                </Menu.Item>
                <Menu.Item key="signup">
                    <Link href="signup"><a>회원가입</a></Link>
                </Menu.Item>
            </Menu>
            <Row gutter={8}>
                <Col xs={24} md={6} >
                    {isLoggedIn ? <UserProfile setIsLoggedIn={setIsLoggedIn} /> : <LoginForm setIsLoggedIn={setIsLoggedIn} />}
                </Col>
                <Col xs={24} md={12}>
                    {children}
                </Col>
                <Col xs={24} md={6}>
                    <a href="https://www.zerocho.com" target="_blank" rel="noreferrer noopener">Made by Zerocho</a>
                </Col>
            </Row>
        </div>
    );
};

AppLayout.propTypes = {
    children: PropTypes.node.isRequired,
}

export default AppLayout;
// LoginForm.js

import React, { useState, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Form, Input, Button } from 'antd';
import Link from 'next/link';
import styled from 'styled-components';

const ButtonWrapper = styled.div`
    margin-top: 10px;
`;

const FormWrapper = styled(Form)`
    padding: 10px;
`;

const LoginForm = ({ setIsLoggedIn }) => {
    const [id, setId] = useState('');
    const [password, setPassword] = useState('');

    const onChangeId = useCallback((e) => {
        setId(e.target.value);
    }, []); // 컴포넌트에 props로 넘겨주는 함수는 useCallback을 써줘야 최적화가 된다.

    const onChangePassword = useCallback((e) => {
        setPassword(e.target.value);
    }, []);

    const onSubmitForm = useCallback(() => {
        console.log(id, password);
        setIsLoggedIn(true);
    }, [id, password]);

    const style = useMemo(() => ({ marginTop: 10 }), []);

    return (
        <FormWrapper onFinish={onSubmitForm}>
            <div>
                <label htmlFor="user-id">아이디</label>
                <br />
                <Input
                    name="user-id"
                    value={id}
                    onChange={onChangeId}
                    required
                />
            </div>
            <div>
                <label htmlFor="user-password">비밀번호</label>
                <br />
                <Input
                    name="user-password"
                    value={password}
                    onChange={onChangePassword}
                    required
                />
            </div>
            <ButtonWrapper style={style}>
                <Button type="primary" htmlType="submit" loading={false}>로그인</Button>
                <Link href="/signup"><a><Button>회원가입</Button></a></Link>
            </ButtonWrapper>
        </FormWrapper>
    );
}

LoginForm.propTypes = {
    setIsLoggedIn: PropTypes.func,
}

export default LoginForm;

(추가 개념) _blank 시 rel="noreferrer noopener" 를 붙여줘야한다.

  • 새 창을 열었을 때, 어떤 창에서부터 넘어온 지 알 수 있다.
    • referrer opener 라는 정보
  • 이 두 개를 이용해서 정보를 탈취할 수 있다.
  • 그러므로 보안의 위협을 없애기 위해서 _blank를 쓸 때, 해당 속성을 써주자.
profile
최근 공부 내용 정리 Notion Link : https://western-hub-b8a.notion.site/Study-5f096d07f23b4676a294b2a2c62151b7

0개의 댓글