본문 바로가기

프론트엔드/리액트

[리액트] 초기세팅 및 실습 5차

[참고]

상세 실습 내용은 노션 프론트 페이지 실습란 참고

 

1. router 독립적으로 사용하기

 - 라우터 재사용성, 컴포넌트간 독립성 높이기

 

1) router.js 파일 분리해 모듈화하기

import Home from 'src/components/pages/Home';
import List from 'src/components/pages/List';
import MyPage from 'src/components/pages/MyPage';
import Login from 'src/components/pages/Login';
import NotFound from "src/components/pages/NotFound";
import Main from 'src/components/pages/Main';
import CONST from 'src/assets/const';

const Router = [
    {
        path: CONST.ROUTER.PATH.HOME, component: Home, name: CONST.ROUTER.NAME.HOME,
        meta: { category: CONST.ROUTER.META.CATEGORY.MAIN, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.WHAT, component: Home, name: CONST.ROUTER.NAME.WHAT,
        meta: { category: CONST.ROUTER.META.CATEGORY.MAIN, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.WHY, component: Home, name: CONST.ROUTER.NAME.WHY,
        meta: { category: CONST.ROUTER.META.CATEGORY.MAIN, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.HOW, component: Home, name: CONST.ROUTER.NAME.HOW,
        meta: { category: CONST.ROUTER.META.CATEGORY.MAIN, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.MYPAGE, component: MyPage, name: CONST.ROUTER.NAME.MYPAGE,
        meta: { category: CONST.ROUTER.META.CATEGORY.MAIN, authorization: true, redirect: CONST.ROUTER.PATH.LOGIN }
    },
    {
        path: CONST.ROUTER.PATH.LIST, component: List, name: CONST.ROUTER.NAME.LIST,
        meta: { category: null, authorization: false }
    },

    {
        path: CONST.ROUTER.PATH.LOGIN, component: Login, name: CONST.ROUTER.NAME.LOGIN,
        meta: { category: CONST.ROUTER.META.CATEGORY.SUB, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.MAIN, component: Main, name: CONST.ROUTER.NAME.MAIN,
        meta: { category: CONST.ROUTER.META.CATEGORY.SUB, authorization: false }
    },
    {
        path: CONST.ROUTER.PATH.NOT_FOUND, component: NotFound, name: CONST.ROUTER.NAME.NOT_FOUND,
        meta: { category: null, authorization: false }
    },
];


export default Router

 

 

2) Page.js 에서 Router 동적으로 생성

변경 전

import Home from '../pages/Home';
import List from '../pages/List';
import MyPage from '../pages/MyPage';
import Login from '../pages/Login';
import NotFound from "../pages/NotFound";
import Main from '../pages/Main';
import CONST from 'src/assets/const';
import { Route, Switch, Redirect } from 'react-router-dom';
import 'src/css/layout/Page.css';

function Page() {
    let loggedIn = true;

    return (
        <div className="Page">
            <Switch>
                <Route exact path="/" component={Home}></Route>
                <Route path={CONST.ROUTER.PATH.MAIN} component={Main}></Route>
                <Route path={CONST.ROUTER.PATH.LIST} component={List}></Route>
                <Route path={CONST.ROUTER.PATH.MYPAGE}>
                    {loggedIn ? <MyPage></MyPage> : <Redirect to={CONST.ROUTER.PATH.LOGIN} />}
                </Route>
                <Route path={CONST.ROUTER.PATH.LOGIN} component={Login}></Route>
                <Route path="*" component={NotFound}></Route>
            </Switch>
        </div>
    );
}

export default Page;

 

변경 후 

import Router from 'src/assets/router';
import { Route, Switch, Redirect } from 'react-router-dom';
import 'src/css/layout/Page.css';

function Page() {
    let loggedIn = true;

    return (
        <div className="Page">
            <Switch>
                {Router.map(ob => {
                    return <Route key={ob.name} exact path={ob.path}
                        render={() =>
                            (ob.meta.authorization && !loggedIn) ?
                                <Redirect to={ob.meta.redirect} /> : <ob.component />
                        } />
                })}
            </Switch>
        </div>
    );
}

export default Page;

 

이 때 저 Redirect 부분을 어떻게 처리할지 고민이 많았음

원래는 render 펑션을 따로 쓰지 않고

<Route key={ob.name} exact path={ob.path} component={ob.component} />

이렇게 component 요소로 넣어줬으나 리다이렉팅 컴포넌트를 추가하려니 component 요소로 전달이 불가능

그래서 모두 render 펑션 요소로 처리함. 

로그인은 구현이 안되어있기 때문에 로그인 유무는 그냥 변수로 박아넣었다.

 

 

3) Navigation.js 에서 데이터 삭제. props로 전달받은 데이터 사용

 - link 가 필요한 모든 컴포넌트는 직접 link 컴포넌트를 사용하지 않고 Navigation을 거쳐서 사용

 - Navigation을 호출하는 컴포넌트도 데이터 직접 하드코딩하지 않고 Router.js에서 가져와 사용

 

ex)

① 헤더에 네비게이션 넣기

import Navigation from "src/components/utilities/Navigation";
import Router from 'src/assets/router';
import CONST from 'src/assets/const';
import 'src/css/layout/Header.css';

function Header(props) {
    const mainNav = Router.filter(ob => ob.meta.category === CONST.ROUTER.META.CATEGORY.MAIN)
    const subNav = Router.filter(ob => ob.meta.category === CONST.ROUTER.META.CATEGORY.SUB)

    return (
        <div className="Header">
            <div className="Title ColorWhite">{CONST.APPLICATION}</div>
            <div className="MainNav">
                <Navigation item={mainNav} class="NavItem ColorWhite Button Size14"></Navigation>
            </div>
            <div className="SubNav">
                <Navigation item={subNav} class="NavItem ColorWhite Button Size14"></Navigation>
            </div>
        </div>
    );
}

export default Header;

 

② Not Found 페이지에 홈으로 가는 링크 넣기

import Navigation from "src/components/utilities/Navigation";
import CONST from 'src/assets/const';
import Router from 'src/assets/router';
import 'src/css/pages/NotFound.css';

function NotFound(props) {
    const navItem = Router.filter(ob => ob.name === CONST.ROUTER.NAME.HOME);

    return (
        <div className="NotFound">
            <div className="SizeRes26">{CONST.TEXT.NOT_FOUND}</div>
            <Navigation item={navItem}></Navigation>
        </div>
    )
}

export default NotFound;

 

 

라우터 관리 파일을 독립적으로 관리하지 않으면 페이지가 추가될 때 마다 일일히 페이지며 링크 설정한 파일들을 다 찾아가서 업데이트 해줘야 하는 번거로움이 큼.

simplePage라는 이름이 무색하게 점점 덩치가 커지니까 이런 자잘한 이중작업이 거슬려서 죄다 뜯어고치는 중.

 

하지만 세부적인 구조들은 이렇게 하는게 리액트 친화적인 방식이 맞는지, 성능적으로는 문제가 없는지 해도해도 모르겠어서 어려움. 혼자 땅파는 기분. 

실제로 현업에서 리액트 사용하는 시니어 개발자들은 어떻게 설계하는지 배우고싶다.

 

 

2. file upload custom Button 

커스텀 파일 업로드 버튼시 문제.

이전에 만든 프로필 사진 업로드 버튼은 icon 버튼을 이용하려고 material ui에서 제공하는 IconButton 컴포넌트를 사용했다.

import IconButton from '@material-ui/core/IconButton';
import PhotoCamera from '@material-ui/icons/PhotoCamera';

/...

return(
	<label htmlFor="profile-upload">
    	<IconButton className="Button ColorWhite" variant="contained" aria-label="upload picture" component="span">
        	<PhotoCamera />
    	</IconButton>
	</label>
)

 

근데 직접 커스텀 버튼을 만드니 문제가 생김.

input 태그를 만들어 css로 hidden 시키고, input 태그를 가르키는 label 태그 안에 버튼 태그를 넣어줬는데,

버튼을 눌러도 동작을 안함.

<input className="Hidden" id="file-upload" type="file" onChange={this.handleUpload} />
<label htmlFor="file-upload">
    <button className="ButtonDarken BorderPrimary ColorPrimary Bold">
        {CONST.TEXT.FILE} {CONST.BUTTON.UPLOAD}
    </button>
</label>

 

material ui는 어떤 원리로 된건지 보니 component="span"으로 button태그 대신 span 태그로 바꿔주는 작업이 들어간 듯.

당장 버튼 태그를 span으로 바꾸니 동작함.

 

하지만 버튼 태그를 쓰고 싶어 방법을 찾아봄

button 태그는 label 안에 쓰는게 아니라 label을 button 안에 써야 하더라. 이렇게 바꿔주니 됨

<input className="Hidden" id="file-upload" type="file" onChange={this.handleUpload} />
<button className="ButtonDarken BorderPrimary ColorPrimary Bold">
    <label htmlFor="file-upload">
        {CONST.TEXT.FILE} {CONST.BUTTON.UPLOAD}
    </label>
</button>

 

 

3. 파일 이름 유효성 체크

 

파일 이름에 사용 불가능한 특수문자들

\ / : * ? " < > |

여기에 공백도 포함해서 불가능한 특수문자는 모두 언더바( _ )로 치환해주는 메서드

    const filenameValidationChecker = (name) => {
        // valid code : 1 | invalid code : 0
        const result = { code : 1 , name : name};  
        const invalidRegs = /[*|\\":/?<> ]/gi;
        if(invalidRegs.test(name)){
            result.code = 0;
            result.name = name.replace(invalidRegs, "_");
        }
        return result;
    }

 

결과를 받은 쪽에서 추가 작업을 하기 위해 valid 여부를 나타내는 code값도 함께 리턴함.

 

 

번외 

파일 이름에서 확장자 분리

    const filnameParser = (name) => {
        const index = name.lastIndexOf('.');
        return index === -1 ? { name: name, type: "" } : { name: name.slice(0, index), type: name.slice(index) }
    }

 

파일명 name과 확장자 type을 분리해서 리턴

 

 

4. tabulator 파일로 다운로드

1) react-tabulator csv download doesn't work

 

tabulator는 무료 라이센스로 파일 다운로드, 프린트 등의 기능을 모두 제공한다.

리액트 전용으로 만들어진 여러 데이터 테이블 라이브러리를 제끼고 얘를 선택한 이유기도 하다.

 

pdf나 xlsx파일 다운은 추가적인 라이브러리를 설치해야 하지만 csv파일은 바로 사용 가능하다.

그런데 다운로드 기능이 동작을 안 함. 에러가 뜨는 것도 아니고 그냥 아무런 반응이 없음.

 

한참을 헤메다가 react-tabulator 깃 페이지의 이슈 항목을 뒤져서 해결방법을 찾았다.

github.com/ngduc/react-tabulator/issues/76 

 

Downloading doesn't work without callbacks · Issue #76 · ngduc/react-tabulator

Title Downloading doesn't work out of the box. Short Description: You have to add dummy downloadDataFormatter and downloadReady callbacks to the options to make it work. I don't know if thi...

github.com

options = { 
	...
	downloadDataFormatter: (data) => data,
	downloadReady: (fileContents, blob) => blob,
}

 

옵션에 이렇게 다운로드 관련 옵션 두줄을 추가해줘야 제대로 동작한다.

 

 

2) 데이터 인코딩

데이터에 한글이 들어있으면 깨진다.

원래 다운로드 기본 방식은 다음과 같이 '파일 형식', '파일명' 을 지정해주면 되지만, 

// table은 ReactTabulator에서 추출한 tabulator 객체

table.download("csv", "data.csv");

 

인코딩 옵션을 추가하려면 이렇게 bom : true 까지 세번째 인자로 전달하면 됨

table.download("csv", "data.csv", { bom: true });

 

If you need the output CSV to include a byte order mark (BOM) to ensure that output with UTF-8 characters can be correctly interpereted across didfferent applications, you should set the bom option to true