[CodeCamp-Week 2] Container / Presenter

·2022년 7월 11일
0

포트폴리오 파일을 작업하다가 길고 긴 import 줄과 return 줄이 꼴보기 싫었다
뭐 하나 수정하려고 하면 이름 대조하면서 하나하나 다 찾아야하는 번거로움이 느껴졌고, Ctrl + F 로 겨우겨우 찾으면서 수정했다

그런 번거로움을 느끼고 난 후 배우라는 커리큘럼의 큰 그림인건지 폴더 구성 패턴을 새롭게 배웠다!

1. Container / Presenter 패턴이란?

Container / Presenter 패턴이란 하나의 index.js 파일을 자바스크립트 부분과 HTML 부분으로 나누어 파일을 따로 관리하는 것을 말한다.
Container는 자바스크립트 (기능) 부분을, Presenter는 return 속 HTML (JSX) 부분을 담당한다.

2. 폴더 구조 살펴보기


포트폴리오 폴더를 예시로 살펴보고자 한다!

pages 폴더

기존엔 pages 폴더 index.js로 전체 관리를 했다면,
해당 페이지의 기능에 따라 폴더를 따로 구분해 준 후 각각 index.js를 생성해주었다.
해당 이미지의 경우 freeboard_write 는 mutation 페이지를, freeboard_detail의 경우 query 페이지를 의미한다.

src 폴더

src 폴더를 새로 만들어 기존 index.js 파일 내에 있던 javascript 기능과 JSX 부분을 빼준다.
해당 이미지의 경우 src 폴더 내에 여러 페이지에서 공통으로 쓰는 commons와 특정 페이지에서만 쓰는 components 폴더를 따로 만들어주었다.
게시판 페이지의 경우 특정 페이지에서만 쓰므로 components 내 board 폴더를 생성하여 게시판 작성, 상세보기, 리스트 조회로 폴더를 나누어주었다.

container와 presenter 파일

src 폴더 내 게시판 작성, 상세보기로 나눠놓은 폴더에서 각각 container.js 파일과 presenter.js 파일을 생성해준다.
pages폴더 안에 있는 index.js 파일에서 javascript 부분을 잘라내기 후 container.js 파일에, JSX 부분을 잘라내기 후 presenter.js 파일에 붙여 넣어준다.

3. pages의 index.js 파일과 container.js, presenter.js 파일 연결해주기

1) 부모, 자식 관계 살펴보기

index.js, container.js, presenter.js를 연결해주기 위해선 부모 자식 관계가 필요하다.
index.js > container.js > presenter.js ( > emotion.js)
이와 같은 부모 자식 관계가 존재하므로, 부모 컴포넌트에 import를 해줘야한다.

  • styles 폴더에 emotion.js가 따로 있을 경우 emotion.js에서 export하여 presenter.js로 import
  • presenter.js에서 export하여 container.js로 import
  • container.js에서 export하여 최종적으로 index.js로 import

2) 코드 살펴보기

(1) index.js

import BoardWrite_Container from '../../src/components/units/board/write/BoardWrite.container'

export default function FreeBoardMutationPage() {
   return <BoardWrite_Container></BoardWrite_Container>
}

(2) container.js

import { useState } from 'react'
import { useMutation, gql } from '@apollo/client'
import { useRouter } from 'next/router'
import BoardWrite_Presenter from './BoardWrite.presenter'

//GraphQl 코드 생성
const CREATE_BOARD = gql `
    mutation createBoard($createBoardInput: CreateBoardInput!) {
        createBoard (createBoardInput: $createBoardInput) {
            _id
            writer
            title
            contents
            youtubeUrl
            images
        }
    }
`

export default function BoardWrite_Container() {
    //mutation 코드 생성
    const [createBoard] = useMutation(CREATE_BOARD)

    //router 코드 생성
    const router = useRouter()

    //state 박스 생성
    const [writer, setWriter] = useState("")
    const [password, setPassword] = useState("")
    const [title, setTitle] = useState("")
    const [contents, setContents] = useState("")
    const [youtubeUrl, setYoutubeUrl] = useState("")
    const [images1, setImages1] = useState("")
    const [images2, setImages2] = useState("")
    const [images3, setImages3] = useState("")

    const [writerError, setWriterError] = useState("")
    const [passwordError, setPasswordError] = useState("")
    const [titleError, setTitleError] = useState("")
    const [contentsError, setContentsError] = useState("")


    const onChangeWriter = (event) => {
        setWriter(event.target.value)
        if (event.target.value !== "") {
            setWriterError("")
        } else if (event.target.value === "") {
        setWriterError("이름을 입력하세요.") }
    }

    const onChangePassword = (event) => {
        setPassword(event.target.value)
        if (event.target.value !== "") {
            setPasswordError("")
        } else if (event.target.value === "") {
        setPasswordError("비밀번호를 입력하세요.") }
    }

    const onChangeTitle = (event) => {
        setTitle(event.target.value)
        if (event.target.value !== "") {
            setTitleError("")
        } else if (event.target.value === "") {
        setTitleError("제목을 입력하세요.") }
    }

    const onChangeContents = (event) => {
        setContents(event.target.value)
        if (event.target.value !== "") {
            setContentsError("")
        } else if (event.target.value === "") {
        setContentsError("내용을 입력하세요.") }
    }

    const onChangeYoutubeUrl = (event) => {
        setYoutubeUrl(event.target.value)
    }

    const onChangeImages1 = (event) => {
        setImages1(event.target.value)
    }

    const onChangeImages2 = (event) => {
        setImages2(event.target.value)
    }

    const onChangeImages3 = (event) => {
        setImages3(event.target.value)
    }

    const onClickFreeboardMutationPage = async() => {
        //error 메세지 설정
        writer ? setWriterError("") : setWriterError("이름을 입력하세요.")
        password ? setPasswordError("") : setPasswordError("비밀번호를 입력하세요.")
        title ? setTitleError("") : setTitleError("제목을 입력하세요.")
        contents ? setContentsError("") : setContentsError("내용을 입력하세요.")

        //mutation 코드 실행
        if (writer && password && title && contents) {
            try{
                const result = await createBoard({
                    variables: {
                        createBoardInput: {
                            writer: writer,
                            password: password,
                            title: title,
                            contents: contents,
                            youtubeUrl: youtubeUrl,
                            images: images1&&images2&&images3
                        }
                    }
                })
                //라우팅할 페이지 설정
                router.push(`/freeboard_detail/${result.data.createBoard._id}`)
                console.log(result)
                } catch (error) {
                    console.log(error.message)
                }
        }
    }

    return <BoardWrite_Presenter
        writerError={writerError}
        passwordError={passwordError}
        titleError={titleError}
        contentsError={contentsError}

        onChangeWriter={onChangeWriter}
        onChangePassword={onChangePassword}
        onChangeTitle={onChangeTitle}
        onChangeContents={onChangeContents}
        onChangeYoutubeUrl={onChangeYoutubeUrl}
        onChangeImages1={onChangeImages1}
        onChangeImages2={onChangeImages2}
        onChangeImages3={onChangeImages3}
        onClickFreeboardMutationPage={onClickFreeboardMutationPage}>
        </BoardWrite_Presenter>
    

}

(3) presenter.js

import {
    Wrapper,
    Wrapper_Header,
    Wrapper_Body,
    Body_Info,
    Body_Title_Input,
    Title,
    Info_Input,
    Input_Error,
    Body_Contents,
    Contents_Input,
    Contents_Input_detail,
    Address_Search,
    Contents_Input_Address,
    Address_Button,
    Body_Title_Input_Photo,
    Photo,
    Photo_Button,
    Body_Title_Input_MainSetting,
    MainSetting,
    MainSetting_Radio,
    Wrapper_Button,
    Mutation_Button
} from '../../../../../styles/freeboard_mutation_emotion'

export default function BoardWrite_Presenter(props) {

    return(
        <Wrapper>
            <Wrapper_Header>
            게시물 등록
            </Wrapper_Header>

            <Wrapper_Body>
                <Body_Info>
                    <Body_Title_Input>
                        <Title>
                            작성자
                        </Title>
                        <Info_Input placeholder='이름을 적어주세요.' type='text' onChange={props.onChangeWriter}></Info_Input>
                        <Input_Error>{props.writerError}</Input_Error>
                    </Body_Title_Input>
                    
                    <Body_Title_Input>
                        <Title>
                            비밀번호
                        </Title>
                        <Info_Input placeholder='비밀번호를 입력해주세요.' type='password' onChange={props.onChangePassword}></Info_Input>
                        <Input_Error>{props.passwordError}</Input_Error>
                    </Body_Title_Input>
                </Body_Info>

                <Body_Contents>
                    <Body_Title_Input>
                        <Title>
                            제목
                        </Title>
                        <Contents_Input placeholder='제목을 작성해주세요.' type='text' onChange={props.onChangeTitle}></Contents_Input>
                        <Input_Error>{props.titleError}</Input_Error>
                    </Body_Title_Input>

                    <Body_Title_Input>
                        <Title>
                            내용
                        </Title>
                        <Contents_Input_detail placeholder='내용을 작성해주세요.' onChange={props.onChangeContents}></Contents_Input_detail>
                        <Input_Error>{props.contentsError}</Input_Error>
                    </Body_Title_Input>

                    <Body_Title_Input>
                        <Title>
                            주소
                        </Title>
                        <Address_Search>
                            <Contents_Input_Address type='num'></Contents_Input_Address>
                            <Address_Button>우편번호 검색</Address_Button>
                        </Address_Search>
                        <Contents_Input type='text'></Contents_Input>
                        <Contents_Input type='text'></Contents_Input>
                    </Body_Title_Input>

                    <Body_Title_Input>
                        <Title>
                            유튜브
                        </Title>
                        <Contents_Input placeholder='링크를 복사해주세요.' type='text' onChange={props.onChangeYoutubeUrl}></Contents_Input>
                    </Body_Title_Input>
                    
                    <Body_Title_Input_Photo>
                        <Title>
                            사진 첨부
                        </Title>
                        <Photo>
                            <Photo_Button onChange={props.onChangeImages1}>+</Photo_Button>
                            <Photo_Button onChange={props.onChangeImages2}>+</Photo_Button>
                            <Photo_Button onChange={props.onChangeImages3}>+</Photo_Button>
                        </Photo>
                    </Body_Title_Input_Photo>
                    
                    <Body_Title_Input_MainSetting>
                        <Title>
                            메인 설정
                        </Title>
                        <MainSetting>
                            <MainSetting_Radio type='radio' name='MainSetting'></MainSetting_Radio> 유튜브
                            <MainSetting_Radio type='radio' name='MainSetting'></MainSetting_Radio> 사진
                        </MainSetting>
                    </Body_Title_Input_MainSetting>

                </Body_Contents>
            </Wrapper_Body>
            
            <Wrapper_Button>
                <Mutation_Button onClick={props.onClickFreeboardMutationPage}>등록하기</Mutation_Button>
            </Wrapper_Button>
        </Wrapper>
    )
}

(4) emotion.js

import styled from "@emotion/styled";

export const Wrapper = styled.div `
    display: flex;
    flex-direction: column;
    align-items: center;

    margin: 100px;
    width: 1200px;
    background-color: #ffffff;
    box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
`

export const Wrapper_Header = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: center;

    width: 100%;
    height: 53px;
    padding: 50px;
    font-size: 36px;
    font-weight: 700;
`

export const Wrapper_Body = styled.div `
    display: flex;
    flex-direction: column;
    align-items: center;

    width: 100%;
    height: 1400px;
    padding: 10px 20px;
    margin-top: 30px;
`

export const Body_Info = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: space-between;

    width: 100%;
    padding: 10px;
`

export const Body_Title_Input = styled.div `
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    margin-bottom: 10px;
`

export const Title = styled.div `
    margin-bottom: 10px;
    font-size: 16px;
    font-weight: 500;
    color: black;
`

export const Info_Input = styled.input `
    width: 486px;
    height: 52px;
    ::placeholder {
        font-size: 16px;
        font-weight: 400;
        color: #c4c4c4;
        line-height: 24px;
        text-align: start;
    }
    margin-bottom: 10px;
`

export const Input_Error = styled.div `
    color: red;
    padding: 5px;
    font-size: 13px;

`

export const Body_Contents = styled.div `
    display: flex;
    flex-direction: column;

    width: 100%;
    padding: 10px;
`

export const Contents_Input = styled.input `
    width: 100%;
    height: 52px;
    ::placeholder {
        font-size: 16px;
        font-weight: 400;
        color: #c4c4c4;
        line-height: 24px;
        text-align: start;
    }
    margin-bottom: 10px;
`

export const Contents_Input_detail = styled.textarea `
    width: 100%;
    height: 480px;
    ::placeholder {
        font-size: 16px;
        font-weight: 400;
        color: #c4c4c4;
        line-height: 24px;
    }
    margin-bottom: 10px;
`

export const Address_Search = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: space-between;

    width: 220px;
    height: 52px;
    margin-bottom: 20px;
`

export const Contents_Input_Address = styled.input `
    width: 77px;
    height: 52px;
`

export const Address_Button = styled.button `
    width: 124px;
    height: 52px;

    background-color: black;
    color: white;
    cursor: pointer;

`

export const Body_Title_Input_Photo = styled.div `
    display: flex;
    flex-direction: column;
    
    width: 300px;
    margin-bottom: 30px;
`

export const Photo = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: space-between;

    width: 100%;
    height: 78px;
`

export const Photo_Button = styled.button `
    width: 78px;
    height: 78px;
    cursor: pointer;
`

export const Body_Title_Input_MainSetting = styled.div `
    display: flex;
    flex-direction: column;

    width: 150px;
`
export const MainSetting = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: space-around;

    font-size: 16px;
    font-weight: 500;
    line-height: 24px;
`


export const MainSetting_Radio = styled.input `

    
`

export const Wrapper_Button = styled.div `
    display: flex;
    flex-direction: row;
    justify-content: center;

    width: 500px;
    padding: 20px 30px 50px 30px;
`

export const Mutation_Button = styled.button `
    width: 179px;
    height: 52px;
    padding: 14px 60px;
    background-color: #ffd600;
    border: 1px solid #FFFFFF;
    font-size: 16px;
    font-weight: 500;
    line-height: 24px;
    cursor: pointer;
`
profile
개발을 개발새발 열심히➰🐶

0개의 댓글