Lastupdate : 2026. 08.26

1. 사전 준비 (라이브러리 설치)

Visual Studio의 [도구] ➔ [NuGet 패키지 관리자] ➔ [설루션용 NuGet 패키지 관리]에서 아래 라이브러리를 검색해서 설치하자.

  • 패키지 이름: NModbus (또는 최신 .NET 환경에서 호환되는 NModbus 포크 버전) (또는 패키지 관리자 콘솔에서 Install-Package NModbus 입력)
  • 설치화면 I.

  • 설치화면 II.

Install-Package NModbus
  • 설치확인화면 III.

사용예: 

using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NModbus;

string serverIp = "172.16.32.108"; // 대상 장치 IP
int port = 502;

CancellationTokenSource cts = new CancellationTokenSource();

Console.WriteLine("[시스템] Modbus TCP 자동 재접속 클라이언트를 시작...");

// 메인 루프: 연결이 끊기면 무한으로 재접속 시도
while (!cts.Token.IsCancellationRequested)
{
    TcpClient client = null;
    IModbusMaster master = null; // 수정됨: IModbusIpMaster -> IModbusMaster

    try
    {
        // 1. 서버 연결 시도
        client = new TcpClient();
        Console.WriteLine($"[연결 시도] {serverIp}:{port} 연결 중...");

        // 연결 타임아웃 설정 (3초 안에 응답 없으면 예외 발생)
        var connectTask = client.ConnectAsync(serverIp, port);
        if (await Task.WhenAny(connectTask, Task.Delay(3000)) != connectTask)
        {
            throw new TimeoutException("연결 시간 초과");
        }

        Console.WriteLine("[연결 성공] 통신을 시작.");

        // 2. NModbus 3.x 버전에 맞는 Master 생성 방식
        var factory = new ModbusFactory();
        master = factory.CreateMaster(client); // 수정됨: CreateIpMaster -> CreateMaster

        // 3. 수신(폴링)을 담당하는 백그라운드 스레드 실행
        await RunCommunicationLoop(master, cts.Token);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[통신 오류 또는 연결 끊김] {ex.Message}");
    }
    finally
    {
        // 사용한 자원(소켓 등) 깨끗이 정리
        master?.Dispose();
        client?.Dispose();
        Console.WriteLine("[자원 해제] 연결을 닫고 재접속을 준비.");
    }

    // 4. 바로 재시도하면 서버에 부하를 주므로 3초 대기 후 재접속 시도
    if (!cts.Token.IsCancellationRequested)
    {
        Console.WriteLine("[재접속 대기] 3초 후 다시 연결을 시도...");
        await Task.Delay(3000, cts.Token);
    }
}

// ==========================================
// 통신 및 데이터 수신 루프 메서드
// ==========================================
async Task RunCommunicationLoop(IModbusMaster master, CancellationToken cancellationToken) // 수정됨
{
    byte slaveId = 1;
    ushort startAddress = 0;
    ushort numRegisters = 10;

    while (!cancellationToken.IsCancellationRequested)
    {
        // 데이터 읽기 (Function 3 예시)
        ushort[] registers = await master.ReadHoldingRegistersAsync(slaveId, startAddress, numRegisters);
        Console.WriteLine($"[수신 데이터] Reg[0]: {registers[0]}, Reg[1]: {registers[1]}");

        // 주기 조절 (1초 간격)
        await Task.Delay(1000, cancellationToken);
    }
}

라이브러리를 사용할지, 아니면 기존처럼 C# 기본 TcpClient로 직접 소켓 통신을 구현할지 고민.

두 방식의 장단점을 명확하게 비교

1. NModbus 라이브러리를 사용할 때

  • 장점:
    • 생산성 최고: Modbus 프로토콜은 바이트 단위의 프레임(Function Code, 주소, 데이터, CRC 체크섬 등)을 엄격하게 지켜야 하는데, 라이브러리가 이 복잡한 과정을 전부 알아서 처리해 줌 (master.ReadHoldingRegistersAsync 한 줄이면 끝난다.)
    • 오류 방지: 통신 과정에서 발생할 수 있는 프로토콜 규칙 오류나 CRC 에러 계산 등을 직접 구현할 필요가 없어 개발 시간이 획기적으로 줄어든다.
  • 단점:
    • 혹시 겪을수 있는 라이브러리 버전(3.x)에 따라 네임스페이스나 메서드 이름이 바뀔 때 문서를 찾아봐야 하는 약간의 러닝커브가 있다.

2. 직접 TcpClient로 구현할 때

  • 장점:
    • 외부 의존성 제로: NuGet 패키지를 쓸 필요가 없으므로 버전 호환성 문제로 골치 아플 일이 전혀 없다.
    • 완벽한 제어: Modbus TCP 패킷(MBAP Header + PDU) 구조를 직접 바이트 배열(byte [])로 조립하고 파싱 하므로, 프로토콜 구조를 뼛속까지 이해할 수 있다.
  • 단점:
    • 구현의 번거로움: 소켓으로 바이트를 보낸 뒤, 응답 데이터의 바이트 배열을 받아와서 Function Code가 에러인지 정상인지, CRC나 트랜잭션 ID는 맞는지 직접 쪼개고 검증하는 코드를 일일이 다 짜야 한다. (생각보다 손이 많이 가고 실수가 발생하기 쉽다.)

💡 최종 추천

  • 안전하고 빠른 개발을 원한다면 👉 NModbus 라이브러리 사용을 추천.
    • 비록 처음에 설정과 버전 차이 때문에 약간의 시행착오가 있었지만, 일단 한번 세팅해 두면 이후부터는 센서나 PLC 데이터를 읽고 쓰는 비즈니스 로직(핵심 기능) 개발에만 집중할 수 있어 훨씬 효율적.
  • 만약 프로토콜을 직접 바이트 단위로 제어하는 재미를 느끼고 싶거나 외부 라이브러리 자체를 배제하고 싶다면 👉 직접 구현도 나쁘지 않지만, 산업용 현장에서 안정성과 유지보수를 고려한다면 검증된 라이브러리를 쓰는 것이 실무 표준이래나 뭐래나.

이미 NModbus 패키지도 설치했고 기본 구조를 잡아가고 있으니, 이번 기회에 라이브러리를 활용해 빠르게 기능을 완성해 볼가 한다 나는~~ 

using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NModbus;

class Program
{
    static async Task Main(string[] args)
    {
        // 1. 설정 정보 (라즈베리파이에서 접속할 대상 장치 IP와 포트)
        string serverIp = "192.168.0.4"; // 대상 장치 IP (필요시 변경)
        int port = 502;                  // Modbus TCP 기본 포트

        CancellationTokenSource cts = new CancellationTokenSource();

        // 콘솔 종료 시 안전하게 취소하기 위한 이벤트 처리 (Ctrl + C)
        Console.CancelKeyPress += (sender, e) =>
        {
            e.Cancel = true;
            cts.Cancel();
            Console.WriteLine("\n[시스템] 종료 요청을 받았습니다. 자원을 정리합니다...");
        };

        Console.WriteLine("[시스템] 라즈베리파이 Modbus TCP 콘솔 클라이언트를 시작합니다...");
        Console.WriteLine($"[대상 정보] IP: {serverIp}, Port: {port}");
        Console.WriteLine("------------------------------------------------------------");

        // 메인 루프: 연결이 끊기면 3초 후 무한 재접속 시도
        while (!cts.Token.IsCancellationRequested)
        {
            TcpClient client = null;
            IModbusMaster master = null;

            try
            {
                // 2. TCP 서버 연결 시도 (3초 타임아웃)
                client = new TcpClient();
                Console.WriteLine($"\n[연결 시도] {serverIp}:{port} 연결 중...");

                var connectTask = client.ConnectAsync(serverIp, port);
                if (await Task.WhenAny(connectTask, Task.Delay(3000, cts.Token)) != connectTask)
                {
                    throw new TimeoutException("연결 시간 초과 (3초)");
                }

                Console.WriteLine("[연결 성공] Modbus 통신을 시작합니다. (1초 주기 요청)");

                // 3. NModbus Master 생성
                var factory = new ModbusFactory();
                master = factory.CreateMaster(client);

                byte slaveId = 1;             // 장비 ID (Unit ID)
                ushort startAddress = 0x0004;   // 시작 주소 (0x04)
                ushort numRegisters = 0x59;   // 요청 개수 (0x59 = 89개)

                // 4. 1초 간격 데이터 요청 및 디스플레이 루프
                while (!cts.Token.IsCancellationRequested)
                {
                    try
                    {
                        // Function Code 4: Read Input Registers
                        ushort[] registers = await master.ReadInputRegistersAsync(slaveId, startAddress, numRegisters);

                        // 수신된 데이터를 1궤도(Hex 형식)로 변환
                        string hexValues = string.Join(", ", Array.ConvertAll(registers, r => $"0x{r:X4}"));

                        // 화면 출력 (디스플레이)
                        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] ➔ [수신 성공] 총 {registers.Length}개 레지스터 읽기 완료");
                        Console.WriteLine($"      Hex 데이터: {hexValues}");

                        // 예시로 첫 번째 레지스터 값(Reg[0])의 십진수 값도 함께 출력
                        if (registers.Length > 0)
                        {
                            Console.WriteLine($"      Reg[0] Dec 값: {registers[0]}");
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"  ➔ [통신 에러 발생]: {ex.Message}");
                        break; // 통신 에러 발생 시 내부 루프를 탈출하여 재접속 수행
                    }

                    // 1초 대기
                    await Task.Delay(1000, cts.Token);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[연결 오류]: {ex.Message}");
            }
            finally
            {
                // 자원 정리
                master?.Dispose();
                client?.Dispose();
                Console.WriteLine("[자원 해제] 연결을 닫았습니다. 3초 후 재접속을 시도합니다.");
            }

            // 재접속 대기 (취소 요청이 없을 경우)
            if (!cts.Token.IsCancellationRequested)
            {
                try
                {
                    await Task.Delay(3000, cts.Token);
                }
                catch (TaskCanceledException)
                {
                    break;
                }
            }
        }

        Console.WriteLine("[시스템] 프로그램이 완전히 종료되었습니다.");
    }
}

CModbusClientTcp.cs

using NModbus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace ENS_RaspberryPi
{
    /// <summary>
    /// 윈도우용 CTcpModbusClient 역할을 대체하는 라즈베리파이용 Modbus TCP 워커 클래스
    /// </summary>
    class CModbusClientTcp
    {
        private readonly string _deviceName;
        private readonly string _ipAddress;
        private readonly int _port;
        private readonly int _intervalMs;

        public CModbusClientTcp(string deviceName, string ipAddress, int port, int intervalMs)
        {
            _deviceName = deviceName;
            _ipAddress = ipAddress;
            _port = port;
            _intervalMs = intervalMs;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            //취소 신호가 들어오기 전까지 계속해서 통신을 시도하고 유지하는 큰 틀의 while 반복문.
            while (!cancellationToken.IsCancellationRequested)
            {
                TcpClient client = null;
                IModbusMaster master = null;

                try
                {
                    // TCP 연결 준비: 소켓 통신을 위한 TcpClient를 만들고, 지정한 IP와 포트로 비동기 연결을 시도.
                    Console.WriteLine($"\n[{_deviceName}] 연결 시도중... ({_ipAddress}:{_port})");

                    client = new TcpClient();
                    var connectTask = client.ConnectAsync(_ipAddress, _port);

                    // 3초 안에 연결되지 않으면 타임아웃 예외 발생 :
                    // 장치가 꺼져있거나 네트워크가 끊겨서 무한 대기에 빠지는 걸 막기 위해,
                    // "딱 3초만 기다려보고 연결 안 되면 에러를 발생시키고(TimeoutException)" 타이머를 건다.
                    if (await Task.WhenAny(connectTask, Task.Delay(3000, cancellationToken)) != connectTask)
                    {
                        throw new TimeoutException("TCP 연결 시간 초과");
                    }

                    // 모드버스 마스터 생성:
                    // TCP 연결이 성공하면 NModbus 라이브러리의 ModbusFactory를 이용해 통신 master를 만든다.
                    // 그리고 슬레이브 ID는 1, 읽기 시작할 주소는 0x0004, 개수는 0x59(89개)로 정의합니다.
                    Console.WriteLine($"[{_deviceName}] 연결 성공! 1초 주기 데이터 요청 시작.");

                    var factory = new ModbusFactory();
                    master = factory.CreateMaster(client);

                    byte unitId = 1;
                    ushort startAddress = 0x0004; // 윈도우 코드에서 사용한 시작 주소 예시
                    ushort registerCount = 0x59;  // 89개 레지스터 요청

                    // 순차적 단일 스레드 비동기 루프 (교착 상태 없음) 데이터 반복 수신 루프:
                    // 1.master.ReadInputRegistersAsync: Func 4번을 이용해 장치로부터 입력 레지스터 데이터를 싹 긁어옴.
                    // 2.DisplayReceivedData: 받아온 데이터를 화면에 출력.
                    // 3.통신 도중 에러가 나면 break로 안쪽 루프를 탈출해 재접속 단계로 넘어감.
                    // 4.await Task.Delay: 설정한 주기(여기서는 0.2초)만큼 쉬었다가 다시 데이터를 요청한다.                    

                    // [수정 포인트 1] 내부 루프에서 복잡한 통신 로직을 별도 메서드로 분리하여 깔끔하게 호출
                    while (!cancellationToken.IsCancellationRequested)
                    {
                        bool isSuccess = await ReadAndDisplayDataAsync(master, unitId, startAddress, registerCount, cancellationToken);

                        // 통신 에러가 발생했다면(false 리턴) 내부 루프를 탈출하여 재접속 단계로 이동
                        if (!isSuccess)
                        {
                            break;
                        }

                        // 설정된 간격 만큼 대기
                        await Task.Delay(_intervalMs, cancellationToken);
                    }
                }
                
                // 통신이 끊기거나 에러가 나서 루프를 빠져나오면
                
                catch (Exception ex)
                {
                    Console.WriteLine($"[{_deviceName}] 연결/통신 오류: {ex.Message}");
                }
                finally // 자원 정리 및 재시도 
                {
                    //열어뒀던 통신망을 정리(Dispose) 메모리 누수막음
                    master?.Dispose();
                    client?.Dispose();
                    Console.WriteLine($"[{_deviceName}] 연결 해제. 3초 후 재접속을 시도합니다.");
                }

                // 3초 뒤에 다시 처음부터 while 문을 돌며 자동 재접속을 시도.
                // 재접속 전 3초 대기
                if (!cancellationToken.IsCancellationRequested)
                {
                    try
                    {
                        await Task.Delay(3000, cancellationToken);
                    }
                    catch (TaskCanceledException)
                    {
                        break;
                    }
                }
            }
        }
        private async Task<bool> ReadAndDisplayDataAsync(IModbusMaster master, byte unitId, ushort startAddress, ushort registerCount, CancellationToken cancellationToken)
        {
            try
            {
                // Function Code 4 (Read Input Registers) - 취소 토큰을 함께 전달하여 안전성 확보
                ushort[] registers = await master.ReadInputRegistersAsync(unitId, startAddress, registerCount);

                // 수신 데이터 디스플레이 출력
                DisplayReceivedData(registers);

                return true; // 성공 시 true 반환
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[{_deviceName}] 통신 에러 발생: {ex.Message}");
                return false; // 에러 발생 시 false를 반환하여 바깥쪽 루프가 재접속을 하도록 유도
            }
        }

        private void DisplayReceivedData(ushort[] registers)
        {
            string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
            Console.WriteLine($"[{timestamp}] [{_deviceName}] 수신 완료 (총 {registers.Length}개 레지스터)");

            // 수신된 데이터를 1궤도(Hex 형식)로 변환
            string hexValues = string.Join(", ", Array.ConvertAll(registers, r => $"0x{r:X4}"));

            // 화면 출력 (디스플레이)
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] ➔ [수신 성공] 총 {registers.Length}개 레지스터 읽기 완료");
            Console.WriteLine($"      Hex 데이터: {hexValues}");

            // 예시로 첫 번째 레지스터 값(Reg[0])의 십진수 값도 함께 출력
            if (registers.Length > 0)
            {
                Console.WriteLine($"  ➔ Reg[0] Dec: {registers[0]}, Hex: 0x{registers[0]:X4}");
            }
        }

    }
}

Program.cs

/*
 * 모듈이름 :
 * 작성일자 : 2026. 08. 25
 * 작성자명 : Hwang KyuSeok
 * 
 * 
 * **/

using ENS_RaspberryPi;  // CModbusClientTcp 클래스 사용
using NModbus;          // NModbus 라이브러리 사용
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine("[시스템] 라즈베리파이 Modbus TCP 클라이언트 시작...");

// 윈도우용 소스의 Cfg_Tcp 설정값들을 빗대어 객체 생성
// (예: 설비용 PCS 또는 피시험품 PCS 접속 정보)
var modbusWorker = new CModbusClientTcp(
    deviceName: "PCS_Equipment",
    ipAddress: "192.168.0.4",
    port: 502,
    intervalMs: 200
);

// 취소 토큰 소스(버튼장치) 생성 : 언제든지 취소 요청 가능
CancellationTokenSource cts = new CancellationTokenSource();

// Ctrl + C 입력 시 안전하게 종료 :
// Ctrl + C를 누를 경우, 프로그램을 강제로 튕기지 않고 안전하게 멈추기 위해
// 위에서 만든 취소 리모컨(cts.Cancel())을 누르도록 이벤트를 등록.
Console.CancelKeyPress += (sender, e) =>
{
    e.Cancel = true;
    cts.Cancel();
    Console.WriteLine("\n[시스템] 종료 중...");
};

// 백그라운드에서 주기적 통신 시작:
// 앞에서 만든 통신 로봇에게 '지금부터 비동기(StartAsync)로 통신을 시작해 줘!' 하고 명령을 내림.
// 프로그램이 멈추지 않고 계속 데이터를 주고받도록 백그라운드에서 실행된다.
var workTask = modbusWorker.StartAsync(cts.Token);

// 프로그램이 종료되지 않도록 대기 :
// 프로그램이 사용자나 시스템에 의해 종료될 때까지 안전하게 기다리다,
// 완전히 끝나면 마지막 문구를 띄우고 프로그램을 종료다.

try
{
    await workTask;
}
catch (TaskCanceledException)
{
    // 정상 취소
}

Console.WriteLine("[시스템] 프로그램이 종료되었습니다.");
using NModbus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace ENS_RaspberryPi
{
    class CModbusClientTcp
    {
        private readonly string _deviceName;
        private readonly string _ipAddress;
        private readonly int _port;
        private readonly int _intervalMs;

        public CModbusClientTcp(string deviceName, string ipAddress, int port, int intervalMs)
        {
            _deviceName = deviceName;
            _ipAddress = ipAddress;
            _port = port;
            _intervalMs = intervalMs;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                TcpClient client = null;
                IModbusMaster master = null;

                try
                {
                    Console.WriteLine($"\n[{_deviceName}] 연결 시도중... ({_ipAddress}:{_port})");

                    client = new TcpClient();
                    var connectTask = client.ConnectAsync(_ipAddress, _port);

                    if (await Task.WhenAny(connectTask, Task.Delay(3000, cancellationToken)) != connectTask)
                    {
                        throw new TimeoutException("TCP 연결 시간 초과");
                    }

                    Console.WriteLine($"[{_deviceName}] 연결 성공! 개별 주기 다중 블록 요청 시작.");

                    var factory = new ModbusFactory();
                    master = factory.CreateMaster(client);
                    byte unitId = 1;

                    // 1초나 0.2초 간격으로 각 블록을 순차적으로 타격하는 루프
                    while (!cancellationToken.IsCancellationRequested)
                    {
                        // 1번 블록 요청
                        if (!await ReadAndDisplayBlockAsync(master, unitId, 0x0004, 0x56, "블록1")) break;
                        await Task.Delay(_intervalMs, cancellationToken);

                        // 2번 블록 요청
                        if (!await ReadAndDisplayBlockAsync(master, unitId, 0x005a, 0x68, "블록2")) break;
                        await Task.Delay(_intervalMs, cancellationToken);

                        // 3번 블록 요청
                        if (!await ReadAndDisplayBlockAsync(master, unitId, 0x00c2, 0x4e, "블록3")) break;
                        await Task.Delay(_intervalMs, cancellationToken);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[{_deviceName}] 연결/통신 오류: {ex.Message}");
                }
                finally
                {
                    master?.Dispose();
                    client?.Dispose();
                    Console.WriteLine($"[{_deviceName}] 연결 해제. 3초 후 재접속을 시도합니다.");
                }

                if (!cancellationToken.IsCancellationRequested)
                {
                    try
                    {
                        await Task.Delay(3000, cancellationToken);
                    }
                    catch (TaskCanceledException)
                    {
                        break;
                    }
                }
            }
        }

        private async Task<bool> ReadAndDisplayBlockAsync(IModbusMaster master, byte unitId, ushort startAddress, ushort registerCount, string blockName)
        {
            try
            {
                // Function Code 4: Read Input Registers
                ushort[] registers = await master.ReadInputRegistersAsync(unitId, startAddress, registerCount);

                DisplayReceivedData(registers, blockName, startAddress);
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[{_deviceName}] [{blockName}] 통신 에러: {ex.Message}");
                return false; // 에러 발생 시 false를 반환해 전체 루프를 끊고 재접속 유도
            }
        }

        private void DisplayReceivedData(ushort[] registers, string blockName, ushort startAddress)
        {
            string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
            string hexValues = string.Join(", ", Array.ConvertAll(registers, r => $"0x{r:X4}"));

            Console.WriteLine($"[{timestamp}] [{_deviceName}] [{blockName}] 수신 성공 (Start: 0x{startAddress:X4}, 개수: {registers.Length})");
            Console.WriteLine($"      Hex: {hexValues}");
        }
    }
}

- 기존에 익숙했던 Visual Studio (Community 2026 버전 기준)를 활용하여, 메인 윈도 PC에서 프로젝트를 생성하고,

- 리눅스(라즈베리파이 4B)에 맞게 빌드한 뒤

- 결과물만 복사해 실행하는 과정을

1단계: 윈도우 PC의 Visual Studio에서 프로젝트 생성하기

  1. Visual Studio 2026을 실행하고 [새 프로젝트 만들기]를 선택
  2. 프로젝트 템플릿 검색창에 "콘솔" 또는 "Console"을 입력.
  3. C# 언어 기반의 [콘솔 앱 (Console App)]을 선택하고 다음으로 넘어간다. (이때. NET 버전은 라즈베리파이에 설치하신 버전에 맞춰. NET 8.0 이상을 선택.)
  4. 프로젝트 이름과 저장 위치를 지정하고 [만들기]를 누르면 기본 코드가 생성.

2단계: 리눅스(라즈베리파이) 타깃에 맞게 빌드(게시)하기

라즈베리파이 4B(64비트 기준)에서 바로 실행할 수 있도록 윈도 PC에서 리눅스용 파일로 변환(Publish)하는 과정.

  1. Visual Studio 상단 메뉴에서 [빌드] ➔ [게시(Publish)...]를 선택합니다. (또는 설루션 탐색기에서 프로젝트 우클릭 ➔ [게시])
  2. 대상(Target) 선택 창이 뜨면 [폴더(Folder)]를 선택하고 다음으로 넘어간다.
  3. 폴더 위치를 지정하고 완료하면 게시 프로필이 생성된다.
  4. 게시 설정 화면에서 우측 상단의 [편집(Edit)] 아이콘을 클릭한다.
  5. 설정 창에서 아래 항목들을 변경한다.
    • 대상 프레임워크(Target Framework): net8.0
    • 배포 모드(Deployment Mode): 프레임워크 종속(Framework-dependent) 또는 단일 파일(Self-contained) 중 선택
      • : 라즈베리파이에 .NET 8 런타임이 이미 설치되어 있다면 용량이 가벼운 프레임워크 종속을, 런타임 설치 없이 단일 파일로 실행하고 싶다면 단일 파일을 선택한다.
    • 대상 런타임(Target Runtime): linux-arm64 (라즈베리파이 4B 64비트 OS 기준)로 변경하고 [저장]을 클릭.
  6. 화면 우측 상단의 [게시(Publish)] 버튼을 클릭.
  7. 빌드가 완료되면 지정한 폴더에 라즈베리파이에서 구동될 실행 파일과 라이브러리 결과물들이 쏙 뽑혀 나오게 된다.

3단계: 결과물을 라즈베리파이로 복사하고 실행하기

이제 윈도우에서 추출된 결과물 파일들을 라즈베리파이로 이동시켜 실행하면 됨.

  1. 파일 전송하기:
    • 방금 게시(Publish) 폴더에 생성된 모든 파일과 폴더를 복사한다.
    • 네트워크를 통해 라즈베리파이로 보내거나, USB 메모리에 담아 라즈베리파이에 꽂은 뒤 원하는 폴더(예: /home/pi/myApp)로 붙여 넣기 한다.
  2. 실행 권한 부여하기 (터미널에서 진행):
    • 라즈베리파이 터미널을 열고, 파일이 복사된 폴더로 이동한다.
    cd /home/pi/myApp
    
    • 메인 프로그램(프로젝트 이름으로 된 실행 파일)에 실행 권한을 부여합니다.
    chmod +x YourProjectName
    
  3. 프로그램 실행하기:
    ./YourProjectName
    

이 워크플로우를 사용면 성능이 제한된 라즈베리파이에서 직접 무겁게 코딩하거나 빌드할 필요 없이, 익숙하고 강력한 Visual Studio에서 편하게 개발한 뒤 결과물만 쏙 넣어 가볍고 빠르게 테스트할 수 있다!


다음은 Windows에서 개발된 프로젝트를 컴파일 후 리눅스용으로 배포하기 위한 위에 설명된 내용일부를 캡처한 화면.

1) [빌드] 메뉴에서 

2) 또는 [설루션 탐색기]에서

3). 폴더선택

4) 구성 Release를 클릭해서 편집한다.

 

5) 설치할 대상으로 프로필 설정

6) <게시> 버튼을 클릭해 파일을 생성한다.

 

생성된 디렉터리를 통째로 복사해서 설치할 장치에 붙여 넣으면 끝!

실행 전에 chmod로 속성을 변경하지 않으면 아래 화면처럼 허가거부!

한번 더 설명  chmode +x 프로젝트명


윈도 작업 PC에서 PuTTY로 SSH 접속해 사용 중, 윈도우와 라즈베리파이 간에 파일을 손쉽고 안전하게 업로드/다운로드할 수 있는 공개 버전(무료) 전용 툴들 추천!

1. WinSCP (가장 추천하는 툴)

PuTTY와 짝꿍처럼 가장 많이 쓰이는 대표적인 무료(Open Source) 파일 전송 프로그램.

  • 특징: 윈도우 탐색기처럼 좌측(내 PC)과 우측(라즈베리파이) 화면이 나뉘어 있어서 마우스 드래그 앤 드롭으로 파일을 편하게 복사·이동할 수 있습니다. SSH(SFTP) 기반이라 PuTTY를 쓸 때 입력하던 라즈베리파이 IP, 계정, 비밀번호 그대로 연동된다.
  • 다운로드 공식 사이트: WinSCP 공식 홈페이지

2. FileZilla (파일질라 클라이언트) - 나는 비추

대용량 파일이나 여러 파일을 동시에 안정적으로 전송할 때 유용한 유명한 무료 FTP/SFTP 클라이언트.

  • 특징: 직관적인 인터페이스와 빠른 전송 속도를 자랑하며, 오픈소스(GPL) 기반의 완전한 무료 공개 버전을 제공.
  • 다운로드 공식 사이트: FileZilla 공식 다운로드 페이지 (클라이언트 버전을 받으시면 된다)

💡 PuTTY와 함께 쓸 때의 꿀팁 (PSCP)

만약 프로그램을 따로 설치하기 귀찮고 명령어 기반이 편하시다면, PuTTY를 설치할 때 함께 다운로드되는 PSCP(PuTTY Secure Copy client)라는 도구를 활용해 윈도우 명령 프롬프트(CMD)에서 명령어로 바로 파일을 보낼 수도 있다.

이 방법은 별도로 작성.

하지만 일반적인 파일 업로드 작업에는 화면을 보며 마우스로 끌어다 넣을 수 있는 WinSCP를 설치해서 사용하시는 것이 가장 편리하고 직관적이지!

1단계: 명령 프롬프트(CMD) 창 열기

  1. 윈도 작업 툴바의 검색창이나 실행 창(Win + R)에 cmd를 입력하고 엔터를 쳐서 명령 프롬프트를 연다.
  2. 윈도의 기본 명령 프롬프트나 PowerShell 어디서든 사용 가능하다.

2단계: 기본 명령어 구조 알아두기

PSCP의 기본적인 전송 명령어 구조는 다음과 같다.

pscp [옵션] [보낼 로컬 파일 경로] [라즈베리파이계정@아이피주소:받을라즈베리파이경로]

3단계: 실제 파일 전송해 보기 (예시)

만약 윈도우 PC의 C:\MyProjects\MyApp 폴더에 있는 MyApp (또는 빌드된 파일들)을 라즈베리파이의 /home/pi/myApp 폴더로 보내고 싶다면 아래와 같이 입력한다.

1) 단일 파일 전송할 때

pscp C:\MyProjects\MyApp\MyApp pi@192.168.0.50:/home/pi/myApp/
  • 192.168.0.50 부분은 본인의 라즈베리파이 IP 주소로 입력해야 한다.
  • 엔터를 치면 라즈베리파이 비밀번호를 물어봅니다. 비밀번호를 입력하면(입력할 때 화면에 별표나 글자가 안 나오는 것은 리눅스 보안상 정상입니다) 전송이 시작된다.

2) 폴더 통째로 전송할 때 (-r 옵션)

빌드된 결과물 폴더 안에 여러 파일과 하위 폴더가 함께 있다면 -r (Recursive) 옵션을 붙여주어야 폴더 통째로 안전하게 복사된다.

Bash
 
pscp -r C:\MyProjects\MyApp\publish\ pi@192.168.0.50:/home/pi/myApp/

💡 처음 접속할 때 참고할 팁

  • 서버 보안 경고(Fingerprint): 처음 pscp를 이용해 라즈베리파이에 접속할 때 Store key in cache? (y/n) 같은 메시지가 뜰 수도 있다. 이때는 y를 입력하고 엔터를 치시면 다음부터는 묻지 않고 바로 전송된다.
  • 경로에 띄어쓰기가 있을 때: 윈도우 파일 경로에 띄어쓰기가 포함되어 있다면 경로 전체를 큰따옴표(" ")로 묶어주면 오류를 방지할 수 있다. (예: pscp -r "C:\My Project\publish\" pi@192.168.0.50:/home/pi/myApp/)

이렇게 CMD창에서 명령어 한 줄만 치면 윈도에서 개발한 결과물이 라즈베리파이로 쏙 전송되므로, 익숙해지면 아주 편리하게 쓰실 수 있다!

라즈베리파이 터미널이나 원격 접속(PuTTY 등) 화면에서 한글이 네모(□) 형태로 깨지는 현상은 시스템에 한글 폰트가 설치되어 있지 않거나, 터미널 프로그램의 문자 인코딩 설정이 UTF-8로 지정되지 않았을 때 발생한다.

이 문제를 해결하기 위해 시스템 설정과 원격 접속 설정 순서대로 해결 방법을 적용해 보자.

1단계: 라즈베리파이 시스템에 한글 폰트 설치하기

라즈베리파이 터미널에 직접 접속하여 아래 명령어를 차례대로 입력해 한글 폰트를 설치한다.

Bash
 
# 1. 패키지 목록 업데이트
sudo apt update

# 2. 나눔고딕 폰트(한글 지원) 설치
sudo apt install -y fonts-nanum

2단계: 로케일(Locale) 및 언어 설정 변경

시스템이 한글(UTF-8)을 올바르게 처리할 수 있도록 언어 환경을 설정한다.

  1. 설정 도구 실행:
  2. Bash
     
    sudo raspi-config
    
  3. 메뉴 이동: Localisation OptionsLocale 순으로 선택한다.
  4. 목록에서 아래 두 항목을 찾아 체크(Space 바)한다.
    • ko_KR.UTF-8 UTF-8
    • en_US.UTF-8 UTF-8 (기본 시스템 언어용)
  5. 기본(Default) 로케일 설정 화면이 나오면 en_US.UTF-8 또는 ko_KR.UTF-8을 선택하고 완료한다.
  6. 설정이 완료되면 라즈베리파이를 재부팅한다.
  7. Bash
     
    sudo reboot
    

3단계: 원격 접속 프로그램(PuTTY) 설정 확인

만약 PC에서 PuTTY를 통해 원격으로 접속했을 때 한글이 깨진다면, PuTTY 자체의 인코딩과 폰트 설정이 잘못되었을 확률이 높다.

  1. 인코딩 설정 (가장 중요)
    • PuTTY 초기 설정 창(또는 접속 중 상단 타이틀바 우클릭 ➔ Change Settings...)을 연다.
    • 좌측 메뉴에서 WindowTranslation으로 이동한다.
    • Received data assumed to be in which character set: 항목을 기존 값에서 UTF-8로 변경한다.
  2. 한글 지원 폰트 설정
    • 좌측 메뉴에서 WindowAppearance로 이동한다.
    • Font settings 아래의 Change... 버튼을 클릭한다.
    • 글꼴을 한글 출력이 원활한 '나눔 고딕', 'Malgun Gothic'(맑은 고딕) 등으로 변경하고 확인을 클릭한다.
  3. 설정 저장
    • 다시 좌측 메뉴 상단의 Session으로 돌아간 후, 저장된 세션을 선택하고 Save 버튼을 눌러 설정을 영구 저장한다.

JetBrains의 라이더(Rider)는 전 세계 개발자들에게 큰 사랑을 받고 있는 개발사인 JetBrains사에서 만든 강력하고 스마트한 C# /. NET 전용 통합 개발 환경(IDE).

비유하자면,. NET(C#, VB.NET 등) 개발을 위해 태어난 '최고급 명품 공구 세트'라고 생각하면 된다.

🚀 라이더(Rider)의 핵심 특징

  • 뛰어난 속도와 가벼움
    • 보통 기능이 많은 IDE는 무거울 것이라 생각하기 쉽지만, 라이더는. NET 기반 툴임에도 불구하고 매우 쾌적하고 빠르게 작동한다. (참고로 내부 엔진은 IntelliJ IDEA의 뼈대를 기반으로 만들어졌다.)
  • 압도적인 코드 분석 및 리팩토링
    • 코드를 작성하는 순간 실수로 발생할 수 있는 버그, 성능 저하 요인, 문법 오류 등을 실시간으로 잡아내고 수정 가이드(Alt + Enter)를 제시해 준다.
    • 코드 구조를 변경하는 리팩토링 기능이 매우 강력하여 대규모 .NET 프로젝트를 관리할 때 엄청난 시간을 아껴준다.
  • 크로스 플랫폼 지원
    • Windows뿐만 아니라 macOS와 Linux 환경에서도 완벽하게 동일한 기능으로 동작한다. 따라서 윈도우 작업 PC나 라즈베리파이 같은 타깃 환경을 다루는. NET 개발자들에게도 유연한 개발 경험을 제공한다.
  • 다양한 기술 스택 통합
    • C# 백엔드 개발뿐만 아니라 ASP.NET Core(웹), Unity(게임 개발), Xamarin/MAUI(모바일) 등 다양한 .NET 생태계를 별도의 복잡한 추가 플러그인 설치 없이 곧바로 지원한다.
    • 웹 프론트엔드 개발에 필요한 JavaScript, TypeScript, HTML, CSS, SQL 등도 함께 다룰 수 있어 풀스택 개발에 최적화되어 있다.

💡 마이크로소프트의 Visual Studio와는 무엇이 다를까?

  • Visual Studio: 마이크로소프트에서 만든 무료(. Community 버전 기준)의 거대하고 전통적인. NET 표준 IDE이다. 윈도 환경에서 매우 강력하다.
  • Rider: JetBrains의 유료 제품(구독형)이지만, JetBrains 고유의 뛰어난 지능형 코드 완성 기능(IntelliSense의 상위 호환 급)과 멀티 플랫폼 지원 덕분에 많은 개발자들이 선호한다. 특히 ReSharper라는 유명한 Visual Studio용 플러그인의 기능이 라이더 안에 기본으로 통째로 녹아들어 있다.

. NET(C#)으로 완성도 높은 애플리케이션이나 장치 제어 프로그램을 만들 때 라이더를 활용하면 코드 작성부터 디버깅까지 훨씬 더 강력하고 편리하게 개발을 진행할 수 있다!

Lastupdate 2026.08.21

라즈베리파이에 C# 프로그램을 실행할 수 있는. NET 8.0 런타임을 설치하는 가장 권장되는 방법은 라즈베리파이의 운영체제인 Raspberry Pi OS의 패키지 관리자인 APT를 이용하는 것. 💡

Microsoft에서 공식적으로 제공하는 패키지 저장소(Repository)를 추가하여 설치하면, 향후 업데이트 관리도 편리.

라즈베리파이에서 sudo apt install -y dotnet-runtime-8.0 명령을 실행했을 때 해당 패키지를 찾지 못하는 이유는 라즈베리파이 OS의 기본 패키지 저장소(Repository)에 마이크로소프트. NET 패키지가 등록되어 있지 않기 때문.

라즈베리파이 OS는 데비안(Debian)이나 우분투(Ubuntu) 기반이지만, 마이크로소프트의 공식 저장소를 수동으로 등록하거나 공식 설치 스크립트를 사용해야만. NET을 올바르게 설치할 수 있다.

가장 간편하고 확실하게 .NET 8을 설치하는 방법 두 가지요.

방법 1: 마이크로소프트 공식 설치 스크립트 사용 (가장 추천)

라즈베리파이 아키텍처(32비트/64비트)에 상관없이 스크립트가 알아서 최적의 버전을 설치해 주므로 가장 안전.

  1. 터미널에 아래 명령어를 입력하여 공식 설치 스크립트를 다운로드하고 실행다.

 

Bash
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0

  1. 설치가 끝나면 시스템에서 dotnet 명령어를 인식할 수 있도록 환경 변수 경로를 등록.
  2. 터미널에 차례대로 입력.
  3. Bash
    echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc
    
    echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> ~/.bashrc
    
    source ~/.bashrc
  4. 설치가 잘 되었는지 확인한다.
  5. Bash
    dotnet --version

방법 2: 패키지 관리자(apt)를 쓰고 싶을 때 (마이크로소프트 저장소 등록)

만약 반드시 apt 명령어로 관리하고 싶다면, 마이크로소프트의 공식 패키지 저장소를 먼저 등록해야 한다.

  1. 마이크로소프트 패키지 서명 키 및 저장소 구성을 등록한다 (라즈베리파이 OS Bookworm/Bullseye 등 데비안 기반 기준).
  2. Bash
    wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
    
    sudo dpkg -i packages-microsoft-prod.deb
    
    rm packages-microsoft-prod.deb
  3. 패키지 목록을 새로고침을 한다.
  4. Bash
     
    sudo apt update
    
  5. 이제 원하는 패키지를 설치.(개발용 SDK도 필요하다면 sudo apt install -y dotnet-sdk-8.0을 입력하면 된다.)
  6. Bash
    sudo apt install -y dotnet-runtime-8.0
    

라즈베리파이 4B를 윈도 PC에서 PuTTY로 원격 접속해 사용할 때 일정 시간이 지나면 발생하는

"Network error: Software caused connection abort" 오류는

유휴 상태(Idle)가 지속될 때 공유기, 라즈베리파이, 또는 윈도 네트워크 세션이 연결을 강제로 끊어버리기 때문에 발생한다.

이 현상을 깔끔하게 해결하는 방법

💡 해결 방법: PuTTY 설정에서 'Keepalive' 설정하기

연결이 끊기는 이유는 양쪽 장치가 서로 살아있는지 확인하는 신호(Heartbeat)를 주고받지 않아 방화벽이나 공유기가 세션을 만료시키기 때문. PuTTY 설정에서 주기적으로 신호를 보내도록 설정하면 간단하게 해결.

1단계: PuTTY 세션 설정창 열기

  • 이미 접속 중인 경우: PuTTY 창 상단 타이틀바를 마우스 우클릭한 뒤 Change Settings... 를 클릭.
  • 새로 켤 경우: PuTTY를 실행한 초기 화면(Session 창) 상태로 진행.

2단계: Keepalive 간격 조절하기

  1. 좌측 하단 메뉴에서 Connection을 클릭.
  2. 우측 화면 중간에 있는 Seconds between keepalives (0 to turn off) 항목을 찾는다.
  3. 이 값을 기본값 0에서 30 또는 60으로 변경한다. (30초~60초마다 신호를 보내 연결이 끊기는 것을 방지한다.)

3단계: 설정 저장하기 (가장 중요!)

이 설정을 앞으로도 계속 적용되도록 저장해야 한다.

  1. 좌측 메뉴 맨 위에 있는 Session으로 다시 이동.
  2. 상단의 Saved Sessions 목록에서 접속 중이거나 기본으로 사용하는 세션 이름(또는 Default Settings)을 클릭.
  3. 우측의 Save 버튼을 클릭하여 설정을 저장.

이렇게 Keepalive 값을 설정해 두시면 일정 시간이 지나도 세션이 유지되므로 "Software caused connection abort" 오류 없이 쾌적하게 라즈베리파이 4B를 원격 제어가 가능!

+ Recent posts