2021.03.21 - [Usage/ReactJS] - Table of Contents 스크롤 메뉴 구현

 

Table of Contents 스크롤 메뉴 구현

createContext를 사용하여 provider, context 생성 Toc.js 생성 메뉴에서 이동 시킬 위치를 각각의 컴포넌트에서 pos로 받아와서 저장 시킬 예정 import React from "react"; import { createContext, useState }..

mugon-devlog.tistory.com

이전 포스트의 확장판입니다.

ToC 스크롤 메뉴가 구현되어 있다는 가정하에 시작하겠습니다.

스크롤의 현재 위치, 동적으로 구현하고픈 페이지의 상단과 하단 위치의 절대 값

//현재 위치가 이곳에 왔을때 active true
const [active, setActive] = useState(false);
const element = useRef(null);

useEffect(() => {
  const onScroll = () => {
    const scrollTop = window.pageYOffset + 100; //현재 위치
    //현재 페이지 상단
    const pagePosTop =
    element.current.getBoundingClientRect().top + window.pageYOffset;
    //현재 페이지 하단
    const pagePosBot =
    element.current.getBoundingClientRect().bottom + window.pageYOffset;
    if (pagePosTop < scrollTop && scrollTop < pagePosBot) {
      setActive(true);
    } else {
      setActive(false);
    }
  };
  if (loc !== 0) {
  	window.addEventListener("scroll", onScroll);
  }
  return () => {
  	window.removeEventListener("scroll", onScroll);
  };
}, [active]);

-------
return(
 <Page id="fourth" ref={element}>
 {* *}
 </Page>
)

스크롤이 페이지 영역에 들어왔을때 나타나게끔 css

styled-components를 이용해 active값을 받아 구현

const IconStyle = styled.img`
  width: 40rem;
  height: 15rem;
  animation: ${(props) => (props.active ? "fadein 3s" : "fadeout 3s")};
  @keyframes fadein {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
  @keyframes fadeout {
    from {
      opacity: 1;
    }
    to {
      opacity: 0;
    }
  }
`;
--------
return (
	<IconStyle src="./icon.png" active={active} />
);

 

 

728x90

createContext를 사용하여 provider, context 생성

Toc.js 생성

메뉴에서 이동 시킬 위치를 각각의 컴포넌트에서 pos로 받아와서 저장 시킬 예정

import React from "react";
import { createContext, useState } from "react";

const ToCContext = createContext([{}, () => {}]);
const TocProvider = ({ children }) => {
  const [pos, setPos] = useState({
    First: 0,
    Second: 0,
    Third: 0,
    Fourth: 0,
  });

  return (
    <ToCContext.Provider value={[pos, setPos]}>{children}</ToCContext.Provider>
  );
};
const { Consumer: ToCConsumer } = ToCContext;
export { TocProvider, ToCConsumer };
export default ToCContext;

페이지의 위치 pos에 저장시키기

먼저, context를 사용하려는 컴포넌트 바깥에서 Provider로 감싸준다.

import logo from "./logo.svg";
import "./App.css";
import Nav from "./Nav";
import First from "./First";
import Second from "./Second";
import Third from "./Third";
import Fourth from "./Fourth";
import { TocProvider } from "./Toc";
import styled from "styled-components";
const BodyStyle = styled.div``;
function App() {
  return (
    <div className="App">
      <TocProvider>
        <Nav />
        <BodyStyle>
          <First />
          <Second />
          <Third />
          <Fourth />
        </BodyStyle>
      </TocProvider>
    </div>
  );
}

export default App;

 

ToC.js에서 생성한 ToCContext의 pos를 useContext로 불러옴

useRef를 이용해서 컴포넌트 First의 ref값을 가져옴

해당 컴포넌트의 절대값을 구하기위해  Viewport의 시작지점을 기준으로 한 상대 좌표인 element.getBoundingClientRect(). top과 window.pageYOffset 전체 페이지에서 얼만큼 scroll 됐는지 픽셀 단위를 알려주는 값을 더 함

import React, { useContext, useEffect, useRef } from "react";
import styled from "styled-components";
import ToCContext from "./Toc";
const FirstStyle = styled.div`
  width: 100%;
  height: 500px;
  background-color: green;
`;
function First() {
  const element = useRef(null);
  const [pos, setPos] = useContext(ToCContext);
  useEffect(() => {
    const rect = element.current.getBoundingClientRect();
    setPos((pos) => ({
      ...pos,
      [element.current.id]: rect.top + window.pageYOffset,
    }));
  }, [setPos]);
  return (
    <FirstStyle id="First" ref={element}>
      first
    </FirstStyle>
  );
}

export default First;

네비게이션 구현

useEffect로 현재 스크롤 위치가 메뉴에 해당하는 컴포넌트 영역에 들어왔는지 체크 하고 들어오면 메뉴 폰트에 bold를 줌

import React, { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import ToCContext from "../../../components/ToC";

const NavFont = styled.a`
  font-family: Noto Sans KR;
  font-weight: ${(props) => props.active};
  font-size: 2.2rem;
  color: #ffffff;
  line-height: 3.2rem;
  text-decoration: none;
`;
function Header({
  list = [
    {
      name: "First",
      href: "First",
    },
    {
      name: "Second",
      href: "Second",
    },
    {
      name: "Third",
      href: "Third",
    },
    {
      name: "Fourth",
      href: "Fourth",
    },
  ],
}) {
  const [active, setActive] = useState(0);
  const [pos] = useContext(ToCContext);

  useEffect(() => {
    const onScroll = () => {
      const scrollTop = window.pageYOffset;
      if (scrollTop < pos.Second) {
        setActive(0);
      }
      if (pos.Second <= scrollTop && scrollTop < pos.Third) {
        setActive(1);
      }
      if (pos.Third <= scrollTop && scrollTop < pos.Fourth) {
        setActive(2);
      }
      if (pos.Fourth <= scrollTop) {
        setActive(3);
      }
    };
    if (pos.Second !== 0 && pos.Thrid !== 0 && pos.Fourth !== 0) {
      window.addEventListener("scroll", onScroll);
    }
    console.log("active", active);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  }, [pos]);

  return (
    <Box>
      <NavBox>
        {list.map((el, idx) => (
          <NavFont
          key={idx}
          href={`#${el.href}`}
          active={idx === active ? "bold" : "regular"}
          >
          {el.name}
          </NavFont>
        ))}
      </NavBox>
    </Box>
  );
}

export default Header;
728x90

메뉴를 눌렀을때 선택된 메뉴에 bold와 underline을 주며 하단 내용을 바꿈

Tab 메뉴의 항목별로 나타낼 항목 정의

const TabUI = {
    1: (
      <TabContentsBoxStyle>
        <TabContentsTextStyle>
          단 한번의 입력으로 자동 작성되는 나만의 낚시 장비 세트
        </TabContentsTextStyle>
        <TabContentsImageStyle src="./assets/my_img1@3x.png"></TabContentsImageStyle>
      </TabContentsBoxStyle>
    ),
    2: (
      <TabContentsBoxStyle>
        <TabContentsTextStyle>
          지역 및 어종으로 나눠서 보는 나만의 낚시 포인트, 어보서비스!
        </TabContentsTextStyle>
        <TabContentsImageStyle src="./assets/my_img2@3x.png"></TabContentsImageStyle>
      </TabContentsBoxStyle>
    ),
  };

Tab 메뉴를 클릭했을때의 이벤트 정의

1. 어떤 메뉴를 클릭했는지

2. 클릭했을때, 클릭 안했을때

3. 클릭 함수

//어떤 버튼을 선택했는지
const [selected, setSelected] = useState(1);
//버튼을 선택했을때 true로 줘서 활성상태로 바꾸기, 클릭 안했을땐 false
const [rightActive, setRightActive] = useState(false);
//기본으로 보여줄 항목은 true
const [leftActive, setLeftActive] = useState(true);
//버튼 이벤트 정의
function selectMenu(props) {
if (props === 1) {
  setSelected(1);
  setLeftActive(true);
  setRightActive(false);
} else if (props === 2) {
  setSelected(2);
  setLeftActive(false);
  setRightActive(true);
}

스타일 컴포넌트에 props 전달해서 css 동적으로 변화

const TabLeftButtonStyle = styled.div`
 font-weight: ${(props)=>props.leftActive ? bold : regular};
 font-decoration : ${(props)=>props.leftActive ? underline : none};
`;

function Tab() {
  const [rightActive, setRightActive] = useState(false);
  const [leftActive, setLeftActive] = useState(true);
  }
    return (
        <TabNavStyle>
			//Tab 버튼
          <TabLeftButtonStyle
            onClick={() => selectMenu(1)}
            //아래와 같이 정의하면 styled에 props로 전달 가능
            leftActive={leftActive}
          >
            나의 낚시 장비
          </TabLeftButtonStyle>         
        </TabNavStyle>
    )
}

export default Tab

전체 코드

const TabLeftButtonStyle = styled.div`
 font-weight: ${(props)=>props.leftActive ? bold : regular};
 font-decoration : ${(props)=>props.leftActive ? underline : none};
`;
const TabRightButtonStyle = styled.div`
 font-weight: ${(props)=>props.RightActive ? bold : regular};
 font-decoration : ${(props)=>props.RightActive ? underline : none};
`;
// tab 메뉴의 버튼 눌렀을때 나타날 컴포넌트 정의
const TabUI = {
    1: (
      <TabContentsBoxStyle>
        <TabContentsTextStyle>
          단 한번의 입력으로 자동 작성되는 나만의 낚시 장비 세트
        </TabContentsTextStyle>
        <TabContentsImageStyle src="./assets/my_img1@3x.png"></TabContentsImageStyle>
      </TabContentsBoxStyle>
    ),
    2: (
      <TabContentsBoxStyle>
        <TabContentsTextStyle>
          지역 및 어종으로 나눠서 보는 나만의 낚시 포인트, 어보서비스!
        </TabContentsTextStyle>
        <TabContentsImageStyle src="./assets/my_img2@3x.png"></TabContentsImageStyle>
      </TabContentsBoxStyle>
    ),
  };
function Tab() {
	//어떤 버튼을 선택했는지
  const [selected, setSelected] = useState(1);
	//버튼을 선택했을때 true로 줘서 활성상태로 바꾸기, 클릭 안했을땐 false
  const [rightActive, setRightActive] = useState(false);
  const [leftActive, setLeftActive] = useState(true);
	//버튼 이벤트 정의
  function selectMenu(props) {
    if (props === 1) {
      setSelected(1);
      setLeftActive(true);
      setRightActive(false);
    } else if (props === 2) {
      setSelected(2);
      setLeftActive(false);
      setRightActive(true);
    }
  }
    return (
        <TabNavStyle>
				//Tab 버튼
          <TabLeftButtonStyle
            onClick={() => selectMenu(1)}
            leftActive={leftActive}
          >
            나의 낚시 장비
          </TabLeftButtonStyle>
          <TabRightButtonStyle
            onClick={() => selectMenu(2)}
            rightActive={rightActive}
          >
            나의 어보
          </TabRightButtonStyle>
        </TabNavStyle>
		//선택된 버튼에 따라 그려지는 컴포넌트
        {TabUI[selected]}
    )
}

export default Tab
728x90

AWS EC2 인스턴스 만들기

프리티어 버전 사용 (스토리지 30GB까지 무료)

기본적인 서버를 구성할 것이기 때문에 모든 포트에 대해 접근 가능하도록 하고 보안 그룹에 특별한 설정을 하지 않았습니다.

새로운 키페어를 생성하시면 인스턴스가 시작됩니다.

키페어는 인스턴스에 접근하기 위해 필요하기에 보관에 주의를 기울이세요

생성후 인스턴스에 들어가서 연결 탭에 들어가면 콘솔로 접속하는 방법을 알려줍니다.

키파일을 다운 받은 위치에서 콘솔창을 엽니다.

그 후 키파일에 대해서 나에게만 읽기 권한이 있도록 접근 권한을 설정해 줍니다. 

chmod 400 test.pem

접근 권한 설정 후 아래 명령어를 적습니다.

** 부분은 인스턴스의 공개 ip 입니다.

ssh -i "test.pem" ec2-user@ec2-**-***-***-***.ap-northeast-2.compute.amazonaws.com

콘솔창에 아래의 내용이 나오면 접속된 것 입니다.

기본 소프트웨어 및 nginx, node, nvm, git 설치

//yum 업데이트 (자동 업데이터 겸 패키지 설치/제거 도구)
sudo yum update
//git 설치
sudo yum install git
//nginx 설치 amazon-linux-extras 이름 확인 (이곳에서 설치가능한 소프트웨어 목록확인 가능)
sudo amazon-linux-extras install nginx1
//nvm 설치 (인터넷 검색)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
//nvm 활성화
. ~/.nvm/nvm.sh
//node 설치
nvm install node

깃에서 프로젝트를 가져옵니다.

ls 명령어로 현재 위치에 존재하는 폴더 목록을 확인하면서 cd 로 이동하시면 됩니다.

git clone {원격 저장소의 프로젝트 리포지토리 주소}
cd {프로젝트 폴더의 절대 경로}
npm install

정적 파일 번들링 및 build 경로 확인

CRA(creat-react-app)으로 만들었기에 run build 명령어를 통해 번들링했습니다.

그 후 build 폴더의 위치를 메모해둡니다.

아래와 같은 경로가 나올 것입니다.

/home/ec2-user/git폴더/build
//react 파일 배포 상태로 만들기
npm run build
//index 위치
cd build
pwd

Nginx 설정

하나의 웹 서비스만 배포할때

nginx의 설정 파일을 수정하도록 엽니다.

a 키를 누르면 수정 모드, 끝나면 esc 후 : wq 를 입력하면 저장 후 종료 , :q 저장안하고 종료 입니다.

sudo vi /etc/nginx/nginx.conf

server 부분에 root, index, try_files 부분을 추가, 수정 해줍니다.

server {
	listen 80;
    server_name _;
    root /home/ec2-user/git폴더/build/; //위에서 pwd로 찾은 경로 입력
    index index.html;
    try_files $uri $uri/ /index.html; //react의 router 때문에 추가
}

여러개의 웹 서비를 배포하기 위한 설정

nginx 설정 파일 수정

sudo vi /etc/nginx/nginx.conf

1. server { } 부분을 주석처리해줍니다.

2. server 블록 위에 include /etc/nginx/sites-enabled/(내가 서비스할 앱이름).conf; 를 작성합니다.

sudo mkdir /etc/nginx/sites-enabled

각 웹 서비스의 설정 파일들을 깔끔하게 관리하기 위해 사용

sites-enabled 디렉토리 내에는 각 웹 서비스의 설정파일들을 위치

하지만 이곳에 직접 파일을 작성해서 위치하지 않고 

sudo mkdir /etc/nginx/sites-available

sites-available 디렉토리에 작성 후 바로가기 링크를 이곳에 위치 시킬 예정 (굳이 안해도됨)

sudo vi /etc/nginx/sites-available/(서비스할 앱이름).conf

아래와 같이 설정

location 뒤에 쓴느 것은 Directive라 불리는 것으로 IP 주소 혹은 도메인의 뒷부분인 URI에 대응하는 부분

해당 URI와 매칭되는 경로로 접속이 시도되었을때 서비스할 내용을 정의

server {
	listen 80;
    location / {
    	root /home/ec2-user/git폴더/build/; 
    	index index.html;
    	try_files $uri $uri/ /index.html; 
    }
}

방금 생성한 설정파일의 바로가기 파일을 /etc/nginx/sites-enabled 폴더에 추가

sudo ln -s /etc/nginx/sites-available/(서비할 앱).conf /etc/nginx/sites-enabled/(서비스할 앱).conf

Nginx 테스트 및 실행

sudo nginx -t

syntax is ok

test is successfull 

두개의 문구가 출력되면 테스트 성공

chmod 711 /home/ec2-user

nginx가 build 디렉토리까지 접근할 때 거치는 디렉토리들에 대해 실행 권한 부여 (500에러 방지)

sudo systemctl enable nginx

ec2 인스턴스를 껐다가 다시 실행 했을때 nginx가 자동으로 실행되도록 설정

sudo systemctl start nginx //nginx 시작
sudo systemctl stop nginx //nginx 종료
sudo systemctl status nginx //nginx 상태 확인

이제 인스턴스의 공개 ip 주소로 접속하면 웹 페이지가 나올 것이다.

728x90

+ Recent posts